반응형

테스트 작성 (Unit Test + Snapshot Test)

 

1. 들어가며

iOS 실무에서 테스트는 “있으면 좋은 것”이 아니라 유지보수 비용을 폭발적으로 줄이는 핵심 무기입니다.
특히 다음과 같은 프로젝트일수록 테스트 중요도는 더욱 커집니다.

  • 팀 단위 협업 프로젝트
  • Feature가 지속적으로 추가되는 앱
  • 비즈니스 로직 복잡도 증가
  • Swift Concurrency(async/await) 사용
  • 다양한 디바이스/언어 대응
  • 리팩토링/모듈화 적용된 아키텍처

이 문서는 iOS 실무 개발자들이 반드시 알아야 할

Unit Test / Snapshot Test / Mocking / Test 구조 / Test 전략을 완전한 실전 기준으로 정리한 가이드입니다.

 

2. 왜 테스트가 중요한가?


2.1 리팩토링을 안전하게 할 수 있음

Unit Test가 있으면 코드 구조 변경이 두렵지 않다.

2.2 버그를 조기 발견

QA 단계 전에 ‘로직 버그’를 먼저 걸러낼 수 있음.

2.3 앱 출시 후 장애 발생 감소

특히 금융·공공·커머스 앱에서 치명적 문제를 방지.

2.4 신규 팀원이 금방 적응 가능

“문서를 읽는 대신 테스트를 보면 동작을 이해”할 수 있음.

 

3. 테스트 종류


3.1 Unit Test (단위 테스트)

  • 가장 빠름
  • 핵심 비즈니스 로직 검증
  • UI 영향 없음

3.2 UI Test(XCTest UI)

  • 실제 UI 흐름 자동 테스트
  • 느리지만 중요

3.3 Snapshot Test

  • 화면 레이아웃이 변경되었는지 감지
  • UI 레이아웃 유지에 매우 효과적

 

4. Unit Test 기본 구조


4.1 XCTestCase

final class LoginUseCaseTests: XCTestCase {

    func test_login_success() async throws {
        // Given
        let repo = MockLoginRepository(result: true)
        let useCase = LoginUseCaseImpl(repository: repo)

        // When
        let result = try await useCase.execute(id: "user", password: "1234")

        // Then
        XCTAssertTrue(result)
    }
}

 

5. Given / When / Then 구조


실무 테스트에서 가장 많이 사용하는 구조.

예시

func test_convert_dto_to_domain() {
    // Given
    let dto = UserProfileDTO(...)

    // When
    let domain = dto.toDomain()

    // Then
    XCTAssertEqual(domain.name, "까칠코더")
}

 

6. Mock 객체 만들기


6.1 Repository Mock

final class MockLoginRepository: LoginRepository {
    let result: Bool
    init(result: Bool) {
        self.result = result
    }

    func login(id: String, password: String) async throws -> Bool {
        result
    }
}

Mock을 활용하면 API 서버 없이도 테스트가 가능하다.

 

7. async/await 테스트 작성법


async 테스트는 XCTest에서 기본 지원한다.

func test_async_fetch() async throws {
    let repo = MockRepo()
    let useCase = FetchItemsUseCase(repo: repo)

    let items = try await useCase.execute()
    XCTAssertEqual(items.count, 3)
}

 

8. Swift Concurrency에서 발생하는 흔한 실수


❌ Task 내부에서 expectation 사용

let expectation = expectation(description: "async test")
Task {
    await something()
    expectation.fulfill()
}
waitForExpectations(timeout: 1)

→ 이렇게 하면 race condition 가능성 ↑

✔️ 올바른 방법

func test_async() async throws {
    let result = try await useCase.execute()
    XCTAssertNotNil(result)
}

 

9. Snapshot Test — UI 깨짐 방지 핵심

Snapshot testing은 UI를 이미지로 캡처해

이전 버전과 비교하여 변화가 발생했는지 확인하는 방식이다.

대표 라이브러리:
- iOSSnapshotTestCase
- SnapshotTesting(Swift) — 추천

 

10. SnapshotTest 예제

import SnapshotTesting

final class ProfileViewTests: XCTestCase {

    func test_profile_view_snapshot() {
        let view = ProfileView(model: mockModel)
        let vc = UIHostingController(rootView: view)

        assertSnapshot(matching: vc, as: .image(on: .iPhone13))
    }
}

Snapshot Test는 UI 변경 사항을 정확히 감지해주며

UI 리팩토링 시 매우 강력하다.

 

11. Snapshot Test 실무 팁


11.1 Device-size 별로 테스트 필요

  • iPhone SE
  • iPhone 13
  • iPhone 14 Pro Max
  • iPad

11.2 Dark Mode / Light Mode 동시 테스트

assertSnapshot(matching: vc, as: .image(on: .iPhone13, traits: .init(userInterfaceStyle: .dark)))

11.3 Dynamic Type 테스트

assertSnapshot(matching: vc, as: .image(on: .iPhone13, traits: .init(preferredContentSizeCategory: .accessibilityExtraLarge)))

 

12. Testable Import로 내부 테스트 가능

@testable import MyApp
  • private은 접근 불가
  • internal까지 테스트 가능
  • framework로 분리된 모듈 테스트에 강력함

 

13. 테스트 가능한 아키텍처 만들기


테스트가 쉬운 아키텍처의 조건:

13.1 비즈니스 로직은 ViewController에 있지 않아야 한다

→ ViewModel/UseCase로 분리

13.2 Repository Interface 존재

→ Mock 교체 용이

13.3 Domain은 순수해야 한다

→ 네트워크, UIKit import 절대 금지

 

14. 코드 커버리지(coverage) 관리

Xcode → Test → Coverage

중요 비즈니스 로직 60~80% 커버리지가 이상적.

테스트를 억지로 100% 만들 필요 없음.

 

15. 테스트 작성 시 흔한 실수

  • ❌ UI Test만 작성 → 느리고 유지보수 어려움
  • ❌ Mock 객체 없이 실제 서버 호출
  • ❌ Unit Test에서 비즈니스 로직과 API 로직을 섞음
  • ❌ 테스트 이름을 의미 없이 작성

✔️ 좋은 테스트 이름 예

test_login_fails_when_password_is_empty()

 

16. CI/CD에 테스트 포함하기


GitHub Actions / CircleCI / Bitrise

xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 14'

테스트 자동화 → 기능 추가/리팩토링 시 안정성 폭증.

 

17. 체크리스트

  •  핵심 비즈니스 로직의 Unit Test 존재
  •  Domain/UseCase 테스트 별도 수행
  •  Mock Repository로 테스트 구조 확립
  •  Snapshot Test로 UI 레이아웃 깨짐 방지
  •  async/await 테스트 시 race condition 방지
  •  테스트 이름은 명확한 동작 설명
  •  CI에서 자동 테스트 실행

 

18. 결론

테스트는 개발 속도를 줄이는 것이 아니라 장기적으로 개발 속도를 압도적으로 빠르게 만드는 투자입니다.
특히 모듈화된 아키텍처에서는 Unit Test + Snapshot Test 조합이 더 큰 효과를 발휘합니다.

테스트 가능한 코드 구조를 갖추면
- 리팩토링 자유도 증가
- 장애 발생 감소
- 유지보수 비용 절감
- 팀 전체 생산성 향상
이라는 압도적인 이점을 얻게 됩니다.

반응형
Posted by 까칠코더
,