LoginSignup
8
5

More than 3 years have passed since last update.

【Swift】ゼロからのCombineフレームワーク - ユニットテストを書いてみる

Posted at

Combineを使ったユニットテストの方法

2つの方法を試してみました。

  1. ライブラリなしでやる
  2. Entwineというテスト補助用のライブラリを使う

テスト対象コード

incrementCounter: PassthroughSubjectsendメソッドが呼ばれたら、自身のcounter: Intに数値を加えて、counterStr: CurrentValueSubjectを更新する単純なモデルです。

テストコードでは、incrementCountersendメソッドの呼び出しにたいして、counterStrが正しく更新されていることをテストします。

CounterViewModel.swift
import 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.swift
func 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を使って、テスト対象のSubjectsendメソッド呼び出しのタイミングを設定したあと、resumeメソッドを呼び出します。

TestableSubscriberをテスト対象のPublisherreceiveすることで、TestableSubscriberrecordedOutputにイベントが記録されます。

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)
}

参考

How to Test Your Combine Publishers
EntwineTest Reference

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5