iOS TDD의 시작

TDD 프로세스

ios-starting-tdd
  • Red Test: 실패하는 테스트를 만든다
  • Green Test: 테스트가 패스하도록 수정한다
  • Refactor: 실제 코드와 테스트 코드를 리팩토링 한다
  • Repeat: 필요한 테스트가 다 만들어질때까지 이 과정을 반복한다

테스트 종류

ios-starting-tdd

Unit Test

  • 단위 코드의 로직적인 부분을 테스트
  • 예를들어, 개별적인 함수가 예상대로 작동하는지 등을 확인
  • 각 테스트 케이스는 독립적으로 실행되어야 함
  • Given → When → Then 순서로 테스트 작성

Integration Test

  • 모듈이나 시스템의 부분들이 상호작용을 잘 하는지 검증
  • Unit 테스트와 비슷하지만, 테스트하는 코드의 범위가 더 큼
  • 이 단계에서 Reference와 비교하는 Snapshot 테스팅을 하기도 함

UI Test

  • 앱의 UI가 사용자와의 상호작용 중에 올바르게 작동하는지 검증
  • 사용자의 상호작용을 자동화하고 시뮬레이션하여 UI의 요소들이 예상대로 작동하는지 확인

테스트의 기본

XCTestCase

  • XCTestCase는 XCTest 프레임워크에서 제공하는 클래스
  • 모든 테스트 케이스의 기본이 되는 클래스이다
  • XCTestCase를 상속 받아서 테스트 작성하기 시작
import XCTest

final class MyUITests: XCTestCase { ... }
  • setUp()tearDown()으로 테스트 라이프 사이클 관리
override func setUp() { ... }
override func tearDown() { ... }
  • setUpWithError()tearDownWithError()로 에러 핸들링 가능
override func setUpWithError() throws { ... }
override func tearDownWithError() throws { ... }
  • 다음 setUptearDown으로 async 코드도 사용 가능
override func setUp() async throws { ... }
override func tearDown() async throws { ... }

@testable

@testable import MyApp
  • @testable을 사용해 import 하면 테스트 클래스에서 open, public, internal에 접근 가능
  • UI 테스트에서는 의도적으로 이를 사용하지 않아 내부 코드를 접근하지 않고, UI 컴포넌트만 접근

Assert 메소드

  • 동일성: XCTAssertEqual, XCTAssertNotEqual
  • Boolean: XCTAssertTrue, XCTAssertFalse
  • Nullable: XCTAssertNil, XCTAssertNotNil
  • 비교: XCTAssertLessThan, XCTAssertGreaterThan, XCTAssertLessThanOrEqual, XCTAssertGreaterThanOrEqual
  • 에러: XCTAssertThrowsError, XCTAssertNoThrow

UI 가져오기

  • accessibilityIdentifier를 사용해 접근성 ID를 지정할수 있다
  • 이 ID로 테스트에서 해당 UI를 접근할수 있다
// SwiftUI 코드
ProfileHomeMomentView().accessibilityIdentifier("ProfileHomeMomentView")

// UIKit 코드
uiview.accessibilityIdentifier = "ProfileHomeMomentView"

// 테스트 코드
app.otherElements["ProfileHomeMomentView"]
  • Xcode console에서 break pointpo 명령어로 UI id 혹은 label 확인 가능
(lldb) po app.otherElements

Output: {
  Other, 0x105832cb0, {{0.0, 0.0}, {393.0, 852.0}}
  Other, 0x1058196d0, {{0.0, 0.0}, {393.0, 852.0}}
  Other, 0x105812c80, {{0.0, 0.0}, {393.0, 852.0}}
  ...
  Other, 0x1058103a0, {{16.0, 61.7}, {28.0, 28.0}}, label: 'gearshape.circle.fill'
  Other, 0x10580f930, {{337.0, 57.7}, {40.0, 36.0}}, label: 'More'
  Other, 0x105809590, {{0.0, 544.0}, {393.0, 697.0}}, identifier: 'ProfileHomeMomentView'
}

PageObject Pattern

  • 주로 자동화된 UI 테스트에 사용되는 디자인 패턴
import XCTest

class ProfileHomeScreen {
    var app: XCUIApplication
    
    init(app: XCUIApplication) {
        self.app = app
    }
    
    var profileTab: XCUIElement {
        let tabBar = app.tabBars["Tab Bar"]
        return tabBar.buttons["My"]
    }
    
    var momentView: XCUIElement {
        app.otherElements["ProfileHomeMomentView"]
    }
}

UI Test 예시

  • PageObject Pattern을 사용한 간단한 UI Test 예시
import XCTest

final class MyUITests: XCTestCase {
    var profileHomeScreen: ProfileHomeScreen!
    
    override func setUpWithError() throws {
        profileHomeScreen = ProfileHomeScreen(app: XCUIApplication())
        profileHomeScreen.app.launch()
    }
    
    func testGivenAccessLevel_whenFriends_thenProfileMomentExists() {
        profileHomeScreen.profileTab.tap()
        XCTAssertTrue(profileHomeScreen.momentView.exists)
    }
}

좋은 테스트

좋은 테스트 조건

F.I.R.S.T

  • Fast: 빠른
  • Independent: 독립적인
  • Repeatable: 반복 가능한
  • Self-Validating: 자체 검증 가능한
  • Timely: 적시의

좋은 명명법

func test_givenAppModel_whenStarted_thenInProgressState()
  • 모든 테스트는 test로 시작해야 한다
  • givenAppModel: AppModel이 SUT(system under test)임을 나타낸다
  • whenStarted: 테스트의 조건 또는 상태 변화이다
  • thenInProgressState: SUT의 상태가 어떠해야 하는지에 대한 확인이다

테스트 팁

Test Coverage

  • 좌측 Test Navigator에서 + 버튼을 눌러 새로운 xctestplan 파일을 만든다
  • Test Plan의 Tests에서 기존의 Test 타겟 추가
  • Test Plan의 Configurations에서 Code Coverage를 On으로 변경 및 타겟 설정
  • 메뉴바 Product → Scheme → Edit Scheme의 Test에서 해당 Test Plan을 디폴트로 지정

테스트 빌드 확인

  • launchArguments를 통해 테스트시에만 필요한 코드를 실행할수 있다.
// 테스트 코드
class MyAppUITests: XCTestCase {
    override func setUp() {
        let app = XCUIApplication()
        app.launchArguments = ["UITests"]
        app.launch()
    }
}

// 일반 코드
if ProcessInfo.processInfo.arguments.contains("UITests") { ... }

스피드업 팁

  • speed 값을 조절해 테스트를 빠르게 진행할 수 있다
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    if ProcessInfo.processInfo.arguments.contains("UITests") {
        UIApplication.shared.keyWindow?.layer.speed = 100
    }
}

class MyAppUITests: XCTestCase {
    override func setUp() {
        let app = XCUIApplication()
        app.launchArguments = ["UITests"]
        app.launch()
    }
}

XCUIElement 팁

  • VoiceOver를 활성화하고 해당 Element를 탭하면 어떤 종류인지 알 수 있다.
  • Element의 종류를 바꾸고 싶다면 버튼의 accessibilityAddTraits를 사용할 수 있다.

시간차 대응

  • 애니메이션이나 네트워크 통신 등으로 UI가 나타나기까지 시간차가 발생하는 경우가 있다.
  • 그럴때는 waitForExistence를 적절히 사용하자.
XCTAssertTrue(login.getStartedButton.waitForExistence(timeout: 2))

Xcode Cloud

  • 여러 종류의 테스트를 Xcode Cloud로 간편하게 자동화 할 수 있다
  • 기회가 되면 다른 글을 통해 더 자세히 알아보겠다
ios-starting-tddios-starting-tdd

참고