はじめに
テスト書いてますか?
テストコードが0のアプリにようやくテストコードを入れるようになり、RxTest/RxBlockingを使いはじめました。
CombineLatest
で複数のObservabelをまとめて観測する値がtuple型のときに、テストで詰まったので解決策を残します。
環境
- macOS Catalina 10.15.3
- Xcode 11.3.1 (11C504)
- RxSwift 5.0.1
Rxのテスト
Model
テストしたいModelを用意します。
今回はuserStream: Observable<(String, Int)>
の値をテストします。
import RxSwift
import RxCocoa
class UserModel {
let name: BehaviorRelay<String> = BehaviorRelay(value: "")
let age: BehaviorRelay<Int> = BehaviorRelay(value: 0)
// outputStream
let userStream: Observable<(String, Int)>
init(name: String, age: Int) {
self.name.accept(name)
self.age.accept(age)
userStream = Observable.combineLatest(self.name, self.age)
}
}
テストコード
それではUserModelのuserStream
のテストを書いてみます。
import XCTest
import RxSwift
import RxCocoa
import RxBlocking
import RxTest
@testable import TestApp
class TestAppTests: XCTestCase {
var userModel: UserModel!
var scheduler: TestScheduler!
var disposeBag: DisposeBag!
override func setUp() {
userModel = UserModel(name: "takutoki", age: 5)
scheduler = TestScheduler(initialClock: 0)
disposeBag = DisposeBag()
super.setUp()
}
func testObservableWithTuple() {
// assertする型を定義
let testObserver = scheduler.createObserver((String, Int).self)
// テスト対象のbind先をtestObserverにする
userModel.userStream
.bind(to: testObserver)
.disposed(by: disposeBag)
// name に流すテストObservable
let nameTestObservable = scheduler.createColdObservable([
Recorded.next(10, "ShirasakaKoume"),
Recorded.next(20, "KoshimizuSachiko"),
Recorded.next(30, "HoshiSyoko")
])
// age に流すテストObservable
let ageTestObservable = scheduler.createColdObservable([
Recorded.next(15, 13),
Recorded.next(25, 14),
Recorded.next(35, 15)
])
nameTestObservable
.bind(to: userModel.name)
.disposed(by: disposeBag)
ageTestObservable
.bind(to: userModel.age)
.disposed(by: disposeBag)
// expect
// 0: 初期値
// 10,20,30: nameが変更されたときに流れる
// 15,25,35: ageが変更されたときに流れる
let expected: [Recorded<Event<(String, Int)>>] = [
Recorded.next(0, ("takutoki", 5)),
Recorded.next(10, ("ShirasakaKoume", 5)),
Recorded.next(15, ("ShirasakaKoume", 13)),
Recorded.next(20, ("KoshimizuSachiko", 13)),
Recorded.next(25, ("KoshimizuSachiko", 14)),
Recorded.next(30, ("HoshiSyoko", 14)),
Recorded.next(35, ("HoshiSyoko", 15))
]
scheduler.start()
XCTAssertEqual(testObserver.events, expected) // Global function 'XCTAssertEqual(_:_:file:line:)' requires that '(String, Int)' conform to 'Equatable'
}
}
XCTAssertEqualの行でコンパイル前にエラーが起きます。
余談ですが、Xcode10.3
ではビルド時
に以下のエラーが発生します。
これだと原因がいまいちはっきりしませんね。
実際の直面したときは以下のエラーだったので、解決法がなかなか見つかりませんでした。
Expression type '()' is ambiguous without more context
どうするか
エラーメッセージにある通りEquatable
に準拠した型同士でassertするようにすれば良いです。
そこでstruct同士でassertを行うようにします。
struct AssertStruct: Equatable {
var name: String
var age: Int
public static func == (lhs: AssertStruct, rhs: AssertStruct) -> Bool {
return lhs.name == rhs.name && lhs.age == rhs.age
}
}
Equatable
を準拠したAssertStruct
を作成して ==
メソッドを実装します。
テストも書き直します。
func testObservableWithTuple() {
// assertする型をAssertStructに変更する
let testObserver = scheduler.createObserver(AssertStruct.self)
// userStreamをAssertStructに変換するoperatorを追加する
userModel.userStream
.map{ AssertStruct(name: $0.0, age: $0.1) }
.bind(to: testObserver)
.disposed(by: disposeBag)
let nameTestObservable = scheduler.createColdObservable([
Recorded.next(10, "ShirasakaKoume"),
Recorded.next(20, "KoshimizuSachiko"),
Recorded.next(30, "HoshiSyoko")
])
let ageTestObservable = scheduler.createColdObservable([
Recorded.next(15, 13),
Recorded.next(25, 14),
Recorded.next(35, 15)
])
nameTestObservable
.bind(to: userModel.name)
.disposed(by: disposeBag)
ageTestObservable
.bind(to: userModel.age)
.disposed(by: disposeBag)
// 期待する値はAssertStructの型に変更する
let expected: [Recorded<Event<AssertStruct>>] = [
Recorded.next(0, AssertStruct(name: "takutoki", age: 5)),
Recorded.next(10, AssertStruct(name: "ShirasakaKoume", age: 5)),
Recorded.next(15, AssertStruct(name: "ShirasakaKoume", age: 13)),
Recorded.next(20, AssertStruct(name: "KoshimizuSachiko", age: 13)),
Recorded.next(25, AssertStruct(name: "KoshimizuSachiko", age: 14)),
Recorded.next(30, AssertStruct(name: "HoshiSyoko", age: 14)),
Recorded.next(35, AssertStruct(name: "HoshiSyoko", age: 15))
]
scheduler.start()
XCTAssertEqual(testObserver.events, expected)
}
最後に
上記の例ではこんなんテストする必要あるんか?
と思いますが、
実際の業務ではoperatorを複数噛ませたりdistinctUntilChangedなど入れたりと複雑な要件になりがちです。
テストでoutputを素早く確認できるのは安心ですね。