반응형

iOS TDD 완벽 가이드: 개념부터 실전까지

1. 들어가며

iOS 개발에서 TDD(Test-Driven Development)는 더 이상 선택이 아닌 필수 전략으로 자리 잡고 있습니다.

특히 대규모 프로젝트 Clean Architecture, Tuist 모듈화 환경에서 테스트 주도 개발은 유지보수성과 안정성을 보장하는 핵심 도구입니다.

많은 개발자들이 “테스트 코드를 언제, 어떻게 작성해야 하는가?”라는 질문을 던집니다.

이 글에서는 TDD의 원리를 Swift/Xcode 환경에 맞게 구체적으로 설명하고,

실제 코드 예제와 함께 테스트 작성 → 실패 → 구현 → 성공 리팩토링의 흐름을 단계별로 살펴봅니다.

 

2. TDD란 무엇인가?

TDD(Test-Driven Development)는 테스트 코드를 먼저 작성하고,

그 테스트를 통과시키기 위한 실제 코드를 나중에 작성하는 개발 방식입니다.

핵심 프로세스는 다음 세 단계로 요약됩니다.

  1. Red: 실패하는 테스트 작성 (기능 명세를 코드로 표현)
  2. Green: 테스트를 통과시키기 위한 최소한의 코드 작성
  3. Refactor: 중복 제거 및 코드 개선

이 세 단계를 반복하며 점진적으로 기능을 완성합니다.

 

3. iOS에서 TDD를 적용하는 이유

항목설명

안정성 확보 기능 추가나 리팩토링 시 기존 코드의 안정성을 보장
자동 회귀 테스트 UI나 네트워크 변경 시 기존 동작이 유지되는지 즉시 확인 가능
명세 기반 개발 테스트 코드 자체가 요구사항을 명확히 정의하는 문서 역할
리팩토링 용이성 테스트가 안전망 역할을 하여 구조 개선 시 부담 감소

 

4. Xcode에서의 TDD 환경 구성

Xcode는 기본적으로 XCTest 프레임워크를 지원합니다.

새 프로젝트 생성 시 “Include Tests” 옵션을 활성화하면 자동으로 Test Target이 함께 만들어집니다.

필수 구성 요소

  • XCTest: 기본 단위 테스트 프레임워크
  • XCUITest: UI 테스트 프레임워크
  • Quick/Nimble: BDD(Behavior-Driven Development) 스타일의 테스트를 위한 오픈소스
  • Tuist: 대규모 모듈 프로젝트 구성 시 자동 테스트 스킴 생성에 활용 가능

구조 예시

MyApp/
 ┣ Sources/
 ┃ ┗ Feature/
 ┃    ┣ LoginViewModel.swift
 ┃    ┗ UserService.swift
 ┣ Tests/
 ┃ ┗ FeatureTests/
 ┃    ┣ LoginViewModelTests.swift
 ┃    ┗ UserServiceTests.swift
 ┗ MyApp.xcodeproj

 

5. 기본 예제: Login 기능의 TDD 흐름

(1) 실패하는 테스트 작성 (Red 단계)

import XCTest
@testable import MyApp

final class LoginViewModelTests: XCTestCase {
    func test_loginSuccess_whenCredentialsAreValid() {
        // Given
        let viewModel = LoginViewModel(service: MockUserService())
        
        // When
        viewModel.login(username: "admin", password: "1234")
        
        // Then
        XCTAssertTrue(viewModel.isLoggedIn)
    }
}

아직 LoginViewModel이나 MockUserService가 구현되어 있지 않기 때문에 컴파일조차 되지 않습니다.

이것이 바로 “Red 단계”입니다.

 

(2) 최소한의 코드로 테스트 통과시키기 (Green 단계)

class LoginViewModel {
    var isLoggedIn = false
    private let service: UserServiceProtocol

    init(service: UserServiceProtocol) {
        self.service = service
    }

    func login(username: String, password: String) {
        if service.validate(username: username, password: password) {
            isLoggedIn = true
        }
    }
}

protocol UserServiceProtocol {
    func validate(username: String, password: String) -> Bool
}

final class MockUserService: UserServiceProtocol {
    func validate(username: String, password: String) -> Bool {
        return username == "admin" && password == "1234"
    }
}

이제 테스트를 실행하면 ✅ 통과(Green)합니다.

 

(3) 리팩토링 (Refactor 단계)

final class LoginViewModel {
    private(set) var isLoggedIn = false
    private let service: UserServiceProtocol

    init(service: UserServiceProtocol) {
        self.service = service
    }

    func login(username: String, password: String) {
        isLoggedIn = service.validate(username: username, password: password)
    }
}

리팩토링 후 다시 테스트를 돌려 모든 테스트가 통과하는지 확인합니다.

 

6. BDD 스타일 테스트 (Quick/Nimble 사용)

import Quick
import Nimble
@testable import MyApp

final class LoginViewModelSpec: QuickSpec {
    override func spec() {
        describe("LoginViewModel") {
            var viewModel: LoginViewModel!

            beforeEach {
                viewModel = LoginViewModel(service: MockUserService())
            }

            context("when credentials are valid") {
                it("logs in successfully") {
                    viewModel.login(username: "admin", password: "1234")
                    expect(viewModel.isLoggedIn).to(beTrue())
                }
            }
        }
    }
}

 

7. 테스트의 종류

테스트 유형설명예시

Unit Test 개별 로직 검증 ViewModel, UseCase
Integration Test 모듈 간 상호작용 검증 Network + Repository
UI Test 사용자 인터페이스 동작 확인 버튼 클릭, 화면 전환
Snapshot Test UI 시각적 변경 감지 Diffable UI 테스트
Performance Test 성능 및 메모리 측정 CoreData 저장 성능

 

8. iOS TDD 실무 적용 팁

  1. 테스트 가능한 구조로 설계하라
  2. 의존성 주입(DI)을 적극 활용하라
  3. 테스트 커버리지 목표 설정
  4. CI/CD와 연동
  5. UI Test는 최소화하라

 

9. Tuist 환경에서의 TDD 구성

import ProjectDescription

let workspace = Workspace(
    name: "MyApp",
    projects: ["Projects/**"],
    generationOptions: .options(
        autogeneratedWorkspaceSchemes: .enabled(codeCoverageMode: .all)
    )
)

 

10. TDD의 장단점

장점단점

버그 조기 발견 초기 개발 속도 저하
명확한 요구사항 반영 테스트 유지보수 비용
리팩토링 안정성 확보 UI 중심 기능 테스트 한계
팀 협업 시 일관성 향상 비즈니스 로직이 자주 바뀔 경우 수정 부담

 

11. 결론

iOS 개발에서 TDD는 단순한 테스트 기법이 아니라 “설계 방법론”입니다.

반응형
Posted by 까칠코더
,