테스트 작성 (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 조합이 더 큰 효과를 발휘합니다.
테스트 가능한 코드 구조를 갖추면
- 리팩토링 자유도 증가
- 장애 발생 감소
- 유지보수 비용 절감
- 팀 전체 생산성 향상
이라는 압도적인 이점을 얻게 됩니다.
'Dev Study > iOS' 카테고리의 다른 글
| iOS 개발자가 많이 하는 실수 - Optional Binding 중첩(if let 지옥) (0) | 2025.12.04 |
|---|---|
| iOS 개발자가 많이 하는 실수 - 옵셔널을 강제 언래핑(!)하기 (0) | 2025.12.04 |
| iOS 색상 토큰(Role-based Color Tokens) 역할 설명 가이드 (0) | 2025.12.04 |
| TCA(The Composable Architecture) 사용 가이드(v1.23.1) (1) | 2025.11.18 |
| 앱 시작 속도 개선(App Launch Optimization) (0) | 2025.11.14 |
| Transition / Animation 최적화 (0) | 2025.11.14 |
| AutoLayout — Hugging / Compression Resistance 이해하기 (0) | 2025.11.14 |
| 모듈화(Modularization)로 빌드 속도 최적화 (0) | 2025.11.14 |

