samsung-ga / woody-iOS-tip

🐶 iOS에 대한 소소한 팁들과 개발하다 마주친 버그 해결기, 그리고 오늘 배운 것들을 모아둔 레포
19 stars 0 forks source link

TDD - App Setup #41

Open samsung-ga opened 2 years ago

samsung-ga commented 2 years ago

TDD App Setup

레이먼드 아저씨 책 - iOS Test-Driven Development by Tutorials 의 세번째 챕터 TDD App Setup에서 배운 내용

테스트 코드 세팅하는 방법과 테스트 방식에 대해 알아보자. 마지막엔 UI 테스팅을 Unit 테스트로 어떻게 할 수 있는지에 대해 간단히 알아보자

Setting

✅ Test Target 생성 및 test class 추가하기

  1. Target에서 Unit Testing Bundle 검색하여 추가

image

  1. cmd + n -> Unit Test Case Class 선택

image

  1. example 코드 삭제

✅ 테스트 방법

  1. 테스트 메소드 옆 왼쪽 다이아몬드 클릭
  2. 클래스 옆 왼쪽 다이아몬드 클릭
  3. Product -> Test (cmd + u)
  4. Test navigator에서 테스트하고 싶은 케이스 클릭
  5. 테스트하고 싶은 케이스에 커서 올려두고 control + option + cmd + u

✅ 테스트 모듈 import하기

앱 타겟은 프레임워크가 아니지만, 모듈이기 떄문에 test 타겟에서 프레임워크처럼 임포트할 수 있다.

모듈이 다르기 때문에 앱 타겟에서 public 접근지정자를 붙여야한다. 즉 접근레벨을 한단계 올려야한다. 하지만 이는 swift type safetry의 이점을 챙길 수 없게된다. 이를 해결하는 방식에는 두가지가 있다.

  1. Testability build setting을 Yes로 만들기. Xcode는 앱타겟을 컴파일 시점에 -enable-testing flag 를 포함시켜 swift 엔티티의 컴파일 모듈의 접근 지정레벨을 높여준다.

  2. @testable 어트리뷰트를 추가한다.

    internal 접근 레벨만 public으로 레벨을 올려준다. open일 경우 public으로 레벨을 낮춘다. fileprivate, private은 접근 지정 레벨이 변하지 않는다.

레퍼런스

✅ 테스트 명명법

testAppModel_whenStarted_isInInProgressState

  1. test 로 시작해야한다.
  2. AppModel : 테스트하고 있는 시스템 (system under test = sut)
  3. whenStarted : 테스트 상태, 혹은 행위 (when)에 해당
  4. iInInProgressState : when 이후에 벌어진 sut의 상태, (결과)

✅ XCTestCase

다른 XTest(Java, Swift, C 등 객체지향언어에서 하는 테스트)에서 하는 테스트 방법과 달리 Swift의 XCTest는 전체 테스트 클래스 (XCTestCase 서브클래싱하는 클래스)에 대해 한 번만 실행되는 라이프 사이클이 없다. 왜일까? XCTestCase의 서브 클래스의 라이프 사이클은 각 테스트 외부에서 관리하고 모든 클래스 상태(프로퍼티 등)는 테스트 메소드 간에 유지된다. 그러므로 각 테스트 메소드가 실행되는 순서에 의존할 수 없기 때문에 setup() tearDown() 메소드를 통해매 테스트마다 클래스 상태 초기화와 정리가 필요하다.

✅ Test Target organization

유닛테스트 그룹화 - 앱과 비슷하게 그룹화하자

Test Target
  ⌊ Cases
     ⌊ Group 1
        ⌊ Tests 1
        ⌊ Tests 2
     ⌊ Group 2
        ⌊ Tests
  ⌊ Mocks
  ⌊ Helper Classes
  ⌊ Helper Extensions

무엇을 테스트해야 할까?

UI 업데이트는 테스트를 어떻게 할까?

UI 자동화를 이용한 UI 테스팅은 Unit테스팅과 거리가 멀지만, 상태 제어 로직(앱 상태)을 뷰 계층에서 분리하면 유닛 테스트로 작성할 수 있다. 예를 들어, 버튼이 눌려 버튼의 텍스트가 변한다면 버튼의 텍스트가 변했다는 것을 테스트할 수 있다.

  func testController_whenStartTapped_buttonLabelIsPause() {
    // when
    whenStartStopPauseCalled()

    // then
    let text = sut.startButton.title(for: .normal)
    XCTAssertEqual(text, AppState.inProgress.nextStateButtonLabel)
  }

위와 같이 startButton의 title을 가져올 수 있다.

  func testController_whenCreated_buttonLabelIsStart() {
    // given

    // when
    sut.viewDidLoad()

    // then
    let text = sut.startButton.title(for: .normal)
    XCTAssertEqual(text, AppState.notStarted.nextStateButtonLabel)
  }

위 코드에서 viewDidLoad를 호출해주었다. 이를 호출하지 않는다면, view가 xib에서 로드가 되지 않기 때문에, UI가 초기화되지 않는다. viewcontroller는 init할 때 view를 로드하지 않고,loadView 라이프 사이클에서 로드해준다.

정리

사용한 테스트 코드


import XCTest
@testable import FitNess

class StepCountControllerTests: XCTestCase {
  //swiftlint:disable implicitly_unwrapped_optional
  var sut: StepCountController!

  override func setUpWithError() throws {
    try super.setUpWithError()
    sut = StepCountController()
  }

  override func tearDownWithError() throws {
    sut = nil
    try super.tearDownWithError()
  }

  // MARK: - When
  private func whenStartStopPauseCalled() {
    sut.startStopPause(nil)
  }

  // MARK: - Initial State

  func testController_whenCreated_buttonLabelIsStart() {
    // given
    sut.viewDidLoad()

    // then
    let text = sut.startButton.title(for: .normal)
    XCTAssertEqual(text, AppState.notStarted.nextStateButtonLabel)
  }

  // MARK: - In Progress

  func testController_whenStartTapped_appIsInProgress() {
    // when
    whenStartStopPauseCalled()

    // then
    let state = AppModel.instance.appState
    XCTAssertEqual(state, AppState.inProgress)
  }

  func testController_whenStartTapped_buttonLabelIsPause() {
    // when
    whenStartStopPauseCalled()

    // then
    let text = sut.startButton.title(for: .normal)
    XCTAssertEqual(text, AppState.inProgress.nextStateButtonLabel)
  }
}