はじめに
この記事は、Xcodeを使う開発者で、XCTestで非同期処理のテストを行う場合に、非同期処理の実行順序を考慮してテストしたい方を対象にしています。
XCTestで非同期のテストを行うとは具体的にはどんなものでしょうか。
例えば下記のようなモジュールでシーケンスが正しく実行されていることを確認したい場合です。
final class TestModule {
enum Event {
case first
case second
case third
}
let publisher = PassthroughSubject<Event, Never>()
func start() {
DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { [weak self] in
self?.publisher.send(.first)
}
DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) { [weak self] in
self?.publisher.send(.second)
}
DispatchQueue.global().asyncAfter(deadline: .now() + 0.3) { [weak self] in
self?.publisher.send(.third)
}
}
}
TestModuleは、PassthroughSubjectを持っており、start()関数が呼ばれるとEventをシーケンシャルに発行していきます。
実際には何某かの外因やロジックでEventなどが発行されていくモジュールが普通だと思いますが、順番が
case first
case second
case third
の通りであることが求められるようなモジュールが対象です。
このようなモジュールを簡易的にテストするためのシンプルなツールを作ってみました。
ツール
下記のツールは、初期化時にfulfill()を発行したい数を渡し、対象となるテストモジュールが非同期を実行するたびにfulfill(Int)を呼び出すことで実行順が正しいかどうかを判定します。実行順が間違っていると、XCTFailします。
final class XCTestExpectationTool {
let expectations: [XCTestExpectation]
/// fulfill()で実行順を与えられたときにチェックするためのカウンター
private var counter: Int = 0
/// fulfill()で実行順が明確でない場合にcounterをシフトさせるためのオフセット値
private var offset: Int = 0
/// スレッドセーフにするためのセマフォ
private let semaphore = DispatchSemaphore(value: 1)
init(count: Int) {
expectations = (0 ..< count).map { count in
XCTestExpectation(description: "\(count)-\(UUID().uuidString)")
}
}
func fulfill(_ index: Int, line: Int = #line) {
semaphore.wait()
defer {
semaphore.signal()
}
guard expectations.count > index+offset else {
XCTFail("❌ 存在しないXCTestExpectationです @line(\(line))")
return
}
guard counter+offset == index+offset else {
XCTFail("❌ 事象の発生順番が間違っています 期待値: \(counter+offset), 値: \(index) @line(\(line))")
return
}
expectations[index+offset].fulfill()
counter += 1
}
func fulfill() {
semaphore.wait()
defer {
semaphore.signal()
}
expectations[counter+offset].fulfill()
offset += 1
}
}
テストケース
このようなテストを書いてみるとテストが成功することが分かります。
一方、fulfill(Int)の順番を変えて実行すると、XCTFailしますしもちろんfulfill(Int)の数が違ったり、同じ実行順を与えてもFailとなります。
func testXCTestExpectationTool() throws {
let tool = XCTestExpectationTool(count: 3)
let testModule = TestModule()
var cancellable = Set<AnyCancellable>()
testModule.publisher
.sink { event in
switch event {
case .first:
tool.fulfill(0)
case .second:
tool.fulfill(1)
case .third:
tool.fulfill(2)
}
}
.store(in: &cancellable)
testModule.start()
wait(for: tool.expectations, timeout: 3)
}
実行順がよくわからない場合
とは言え、実行順がブレてしまって分からないこともあります。
その場合は、実行順をしてしないfulfill()を呼び出せばOKです。
ただし、expectationの総数は一致していなければなりません。
func testXCTestExpectationTool順番わからない() throws {
let tool = XCTestExpectationTool(count: 5)
tool.fulfill(0)
tool.fulfill()
tool.fulfill(1)
tool.fulfill(2)
tool.fulfill()
wait(for: tool.expectations, timeout: 3600)
}
↑こんな感じですね。
複雑なテストには向かないですが、シンプルに非同期モジュールをテストするときにご活用ください。
もし間違いの指摘や質問があれば、お声がけください・・(コワイ)