- 投稿日:2020-08-05T08:40:36+09:00
【Swift】ゼロからのCombineフレームワーク - ユニットテストを書いてみる
Combineを使ったユニットテストの方法
2つの方法を試してみました。
- ライブラリなしでやる
- Entwineというテスト補助用のライブラリを使う
テスト対象コード
incrementCounter: PassthroughSubject
のsend
メソッドが呼ばれたら、自身のcounter: Int
に数値を加えて、counterStr: CurrentValueSubject
を更新する単純なモデルです。テストコードでは、
incrementCounter
のsend
メソッドの呼び出しにたいして、counterStr
が正しく更新されていることをテストします。CounterViewModel.swiftimport Combine import Foundation protocol CounterViewModelProtocol { var incrementCounter: PassthroughSubject<Int, Never> { get } var counterStr: CurrentValueSubject<String, Never>! { get } } class CounterViewModel: CounterViewModelProtocol { var incrementCounter: PassthroughSubject<Int, Never> = .init() var counterStr: CurrentValueSubject<String, Never>! private var counter: Int = 0 private var cancellables = Set<AnyCancellable>() init() { counterStr = CurrentValueSubject("\(counter)") incrementCounter .sink(receiveValue: { [weak self] increment in if let self = self { self.counter += increment self.counterStr.send("\(self.counter)") } }).store(in: &cancellables) } }ライブラリなしでテストする
How to Test Your Combine Publishersを参考にしました。
テスト補助用のexpectValue
というメソッドにPublisher
と期待される値の配列を渡して、wait
します。CounterViewModelTests.swiftfunc testCounterStr() { let viewModel = CounterViewModel() let expectValues = ["0", "2", "5"] let result = expectValue(of: viewModel.counterStr, equals: expectValues) viewModel.incrementCounter.send(2) viewModel.incrementCounter.send(3) wait(for: [result.expectation], timeout: 1) }テスト補助用のメソッド
extension XCTestCase { typealias CompetionResult = (expectation: XCTestExpectation, cancellable: AnyCancellable) func expectValue<T: Publisher>( of publisher: T, timeout: TimeInterval = 2, file: StaticString = #file, line: UInt = #line, equals: [T.Output] ) -> CompetionResult where T.Output: Equatable { let exp = expectation(description: "Correct values of " + String(describing: publisher)) var mutableEquals = equals let cancellable = publisher .sink(receiveCompletion: { _ in }, receiveValue: { value in if value == mutableEquals.first { mutableEquals.remove(at: 0) if mutableEquals.isEmpty { exp.fulfill() } } }) return (exp, cancellable) } }Entwineを使ってテストする
テスト用に用意された
TestScheduler
を使って、テスト対象のSubject
のsend
メソッド呼び出しのタイミングを設定したあと、resume
メソッドを呼び出します。
TestableSubscriber
をテスト対象のPublisher
にreceive
することで、TestableSubscriber
のrecordedOutput
にイベントが記録されます。func testCounterStrWithEntWine() { let scheduler = TestScheduler(initialClock: 0) let incrementCounter = viewModel.incrementCounter scheduler.schedule(after: 100) { incrementCounter.send(2) } scheduler.schedule(after: 200) { incrementCounter.send(3) } let subscriber = scheduler.createTestableSubscriber(String.self, Never.self) viewModel.counterStr.receive(subscriber: subscriber) scheduler.resume() let expected: TestSequence<String, Never> = [ (000, .subscription), (000, .input("0")), (100, .input("2")), (200, .input("5")), ] XCTAssertEqual(subscriber.recordedOutput, expected) }参考