관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - Swift] 2. 유닛 테스트 방법 - Quick과 Nimble을 이용한 테스트 코드 구현 방법 (BDD, sut, dummy, mock, stub) 본문

iOS 응용 (swift)

[iOS - Swift] 2. 유닛 테스트 방법 - Quick과 Nimble을 이용한 테스트 코드 구현 방법 (BDD, sut, dummy, mock, stub)

jake-kim 2022. 12. 11. 22:33

1. 유닛 테스트 방법 - Dependency Injection (@Injected) 주입 구조

2. 유닛 테스트 방법 - Quick과 Nimble을 이용한 테스트 코드 구현 방법 <

3. 유닛 테스트 방법 - RxExpect를 이용한 Rx관련 비동기 테스트 코드 구현 방법

번외) 유닛 테스트 방법 - XCTest와 RxSwift만을 이용한 비동기 테스트 구현 방법

 

* 예제 코드는 이전 Dependency Injection에서 사용된 구조를 사용하므로, 이전 글 먼저 참고

테스트 용어

  • sut (System Under Test): 테스트를 하려는 대상
  • dummy: 속성이나 메서드의 인자값
  • mock: 특정 조건에서 원하는 메서드를 실행하는지에 관한 행위 검증에 기대되는 값 (메소드 리턴값 등..)
  • stub: 객체의 상태와 같은 상태 검증에 관한 기대되는 값

가장 일반적인 테스트 방법

BDD (Behavior Deriven Development) - 제한적인 상황을 고려한 인풋에 대한 결과값을 테스트하는 방법

  • Given - 테스트 조건
  • When - 인풋
  • Then - 아웃풋 (= 기대되는 값)
// https://medium.com/@lucianoalmeida1/quick-and-nimble-behavior-driven-development-in-swift-e27d4b213857

Given - The user is logged in
When - The user taps the profile picture
Then - It should be redirected to the profile screen

cf) TDD와 BDD의 차이

  • TDD는 제한적인 상황이 없고, BDD는 제한적인 상황이 있는 상태

Quick과 Nimble

  • Quick
    • BDD 구조를 사용할 수 있게 프레임워크에서 제공
    • describe - context - it 형식으로 메소드와 클로저로 제공 (각각 Given - When - Then)
    • QuickSpec이라는 것을 override하고 spec() 메소드 재정의
    • 메소드 안에서 describe - context - it을 사용할 수 있고, QuickSpec은 XCTestCase의 typealias이므로 XCTestCase 모듈 사용이 가능
import XCTest
import Nimble
import Quick

final class ExDITests: QuickSpec {
    override func spec() {
        describe("The user is logged in") {
            // logged in
            
            context("The user taps the profile picture") {
                // taps picture
                
                it("It should be redirected to the profile screen") {
                    // redirected screen
                    // 검증
                }
            }
        }
    }
}
  • suite - 초기화 (1회 실행)
    • beforeSuite (= setup)
    • afterSuite (= teardown)
  • each - suite는 최초 한번만 호출되지만, each는 선언하는 부분 모두 동작하므로 context가 여러개 있는 경우 그 클로저에서 새로운 값을 setup할때 each로 초기화
    • beforeEach
    • afterEach

ex) describe안에 beforeSuite, afterSuite를 놓고 / context안에 beforeEach, afterEach 사용

실행 순서: describe - context - beforeSuite - beforeEach - it - afterEach - afterSuite

 

(afterSuite가 실행 안되는 이슈가 존재하여, 혹시 이유를 아신다면 코멘트 부탁드립니다.)

// describe - context - beforeSuite - beforeEach - it - afterEach 순서로 실행
final class SomeExample2Tests: QuickSpec {
    
    override func spec() {
        describe("조건") {
            var someStub: Int!
            var sut: Calculator!

            beforeSuite {
                someStub = 3
                sut = Calculator()
            }
            
            afterSuite {
                someStub = nil
                sut = nil
            }

            context("액션") {
                var value: Int!

                beforeEach {
                    value = sut.plus(0, 3)
                }

                afterEach {
                    value = nil
                }

                it("결과") {
					// TODO: 검증
                }
            }
        }
    }
}
  • Nimble
    • 위 it 단계에서 검증할 때, (기댓값과 결과값을 비교할때 사용)
    • 선언 형태로 사용이 가능하여 가독성이 높은 장점이 존재
// https://github.com/Quick/Nimble

expect(1 + 1).to(equal(2))
expect(1.2).to(beCloseTo(1.1, within: 0.1))
expect(3) > 2
expect("seahorse").to(contain("sea"))
expect(["Atlantic", "Pacific"]).toNot(contain("Mississippi"))
expect(ocean.isClean).toEventually(beTruthy())
expect(seagull.squawk).toNot(equal("Oh, hello there!"))
expect(seagull.squawk).notTo(equal("Oh, hello there!"))

ex) nimble까지 적용한 예제

final class SomeExample2Tests: QuickSpec {
    
    override func spec() {
        describe("조건") {
            print("describe")
            var someStub: Int!
            var sut: Calculator!

            beforeSuite {
                print("beforeSuite")
                someStub = 3
                sut = Calculator()
            }
            
            afterSuite {
                print("afterSuite")
                someStub = nil
                sut = nil
            }

            context("액션") {
                print("context")
                var value: Int!

                beforeEach {
                    print("beforeEach")
                    value = sut.plus(0, 3)
                }

                afterEach {
                    print("afterEach")
                    value = nil
                }

                it("결과") {
                    print("it")
                    expect(value)
                        .to(equal(someStub))
                }
            }
        }
    }
}

출력)

describe
context
Test Suite 'SomeExample2Tests' started at 2022-12-11 01:02:00.804
Test Case '-[ExDITests.SomeExample2Tests 조건__액션__결과:]' started.
beforeSuite
beforeEach
it
afterEach
Test Case '-[ExDITests.SomeExample2Tests 조건__액션__결과:]' passed (0.001 seconds).
Test Suite 'SomeExample2Tests' passed at 2022-12-11 01:02:00.806.
	 Executed 1 test, with 0 failures (0 unexpected) in 0.001 (0.002) seconds

DI 테스트 예제

* @Injected와 Dependency Store를 이용한 코드 준비 (이전 포스팅 글 참고)

  • 테스트 내용
    • ConfigService를 이용하는 ViewModel을 검증
    • ConfigService를 Stub으로 하나 만들고 stub으로 동작했을때 ViewModel에서 기대하는 값이 나오는지 테스트
  • ConfigService를 Stub으로 하나 정의
final class StubConfigService: ConfigServiceType {
    private var key = ""
    
    var secretKey: String {
        key
    }
    
    func saveSecretKey(_ key: String) {
        self.key = key + "@"
    }
}
  • describe - context - it 단계로 테스트
    • 편의상 afterSuite, afterEach 생략
    • DepenencyStore()에 configService의 인스턴스를 stub 데이터로 변경하고 viewModel에 적용
final class ExDITests: QuickSpec {
    override func spec() {
        describe("The user is logged in") {
            var sut: ViewModel!
            
            beforeSuite {
                let ds = DependencyStore()
                let stubConfigService = StubConfigService()
                
                ds.register(stubConfigService, for: ConfigServiceType.self)
                sut = ds.execute {
                    ViewModel()
                }
            }

            context("The user tap button") {
                beforeEach {
                    sut.textInput("jake-ios")
                    sut.tapStoreButton()
                }

                it("Get access token") {
                    expect(sut.currentSecretKey)
                        .to(equal("jake-ios@"))
                }
            }
        }
    }
}

/*
 describe
 context
 Test Suite 'ExDITests' started at 2022-12-11 01:54:53.518
 Test Case '-[ExDITests.ExDITests The_user_is_logged_in__The_user_tap_button__Get_access_token:]' started.
 beforeSuite
 Test Case '-[ExDITests.ExDITests The_user_is_logged_in__The_user_tap_button__Get_access_token:]' passed (0.004 seconds).
 Test Suite 'ExDITests' passed at 2022-12-11 01:54:53.523.
      Executed 1 test, with 0 failures (0 unexpected) in 0.004 (0.005) seconds
 */

 

(Quick, Nimble로 테스트하는 실전 코드는 이 포스팅 글 참고)

 

* 전체 코드: https://github.com/JK0369/ExDI

* 참고

https://mokacoding.com/blog/quick-beforesuite-aftersuite-behaviour/

https://azderica.github.io/00-test-mock-and-stub/

https://johngrib.github.io/wiki/test-terms/

https://github.com/Quick/Nimble

https://medium.com/towards-data-science/tdd-explained-with-an-example-738d702f87e

https://medium.com/@lucianoalmeida1/quick-and-nimble-behavior-driven-development-in-swift-e27d4b213857

https://github.com/Quick/Quick/tree/main/Documentation/ko-kr

 

Comments