(2016-01-04: セットアップに RxTests を用いるよう修正しました)
Reactive Extensions(Rx)には TestScheduler というユニットテスト用のスケジューラがあります。
TestScheduler を使うと時間経過をシミュレートできるので、例えば ある時点においてObservableが出力した値をテストすることができます。
この記事では、RxSwift でのTestScheduler の使いかたとサンプルコードを紹介します。
セットアップ(追記: 2016-01-04)
2016年1月2日に RxSwift 2.0.0 がリリースされました。
2.0.0 では RxTests というプロジェクトが追加されており、これを使うと TestScheduler をテストコードで利用できます。
There is a new project called RxTests that has virtual time test scheduler and mock observable sequences.
> https://github.com/ReactiveX/RxSwift/issues/305#issuecomment-168078426
使いかたとしては Tests プロジェクトに RxTests をインポートするだけです。
詳細についてはソースコードのほうを参照してください。
セットアップ(旧版)
2015年12月時点で RxSwift の TestScheduler はコミットされていますが 公開されていません:
yes, the plan is to publicly expose TestScheduler, and I would like to expose a bunch of other test mock classes.
https://github.com/ReactiveX/RxSwift/issues/305
そのため今回は TestScheduler 関連のファイルをプロジェクトのTests ターゲットにコピーしました:
使いかた
TestScheduler を使うには 実際の時刻でなく仮想的な時刻を基準として Observable にイベントを出力させ・Observable を操作します。
仮想的な時刻(VirtualTimeSchedulerBase.Time
)は整数で表します:
typealias Time = Int
Time
を指定してObservable にイベントを出力させるメソッドが TestScheduler にあります:
let xs = scheduler.createHotObservable([ //モックのHot Observableを作成
next(70, 1), //時刻 70 に 値1 を出力
next(110, 2), //110 に 2 を出力
next(220, 3), //220 に 3 を出力
いっぽう ある時刻ちょうどに Observable を操作するメソッドもあります:
scheduler.scheduleAt(100) { subject = BehaviorSubject<Int>(value: 100) } //時刻 100 に BehaviorSubject<Int> を作成
scheduler.scheduleAt(200) { subscription = xs.subscribe(subject) }
// 200 に subject をモックに subscribe させる
RxSwift の BehaviorSubjectTest.swift をアレンジしたサンプルコードを抜粋します。
func test_BehaviorSubject() {
let scheduler = TestScheduler(initialClock: 0) // TestScheduler を作成
let xs = scheduler.createHotObservable([ // Hot Observable
next(70, 1),
next(110, 2),
next(220, 3),
next(270, 4),
next(340, 5),
next(410, 6),
next(520, 7),
next(630, 8),
next(710, 9),
next(870, 10),
next(940, 11),
next(1020, 12)
])
var subject: BehaviorSubject<Int>! = nil // テスト対象の BehaviorSubject
var subscription: Disposable! = nil
let results1 = scheduler.createObserver(Int) // subject の出力結果を受け取る results1
var subscription1: Disposable! = nil
scheduler.scheduleAt(100) { subject = BehaviorSubject<Int>(value: 100) }
scheduler.scheduleAt(200) { subscription = xs.subscribe(subject) }
scheduler.scheduleAt(300) { subscription1 = subject.subscribe(results1) } // results1 に subject を subscribe させる
scheduler.scheduleAt(500) { subject.onCompleted() }
scheduler.scheduleAt(600) { subscription1.dispose() }
scheduler.scheduleAt(1000) { subscription.dispose() }
scheduler.start()
XCTAssertEqual(results1.events, [ // results1 の受け取ったイベントを期待値と照合:
next(300, 4),
next(340, 5),
next(410, 6),
completed(500)
])
}
ポイントとしては モックの xs
が Hot Observable なので subscribe せずとも イベントを出力するという点です。
反対に、モックが Cold Observable なら subscribe するまでイベントを出力しません。subscribe してから 指定した時間(createメソッドの第1引数)が経過したときにイベントを出力します。
なお、Hot Observable と Cold Observable の違いについては別の記事をどうぞ。
例) ViewModel の状態遷移をテストする
TestScheduler を用いて MVVM アーキテクチャにおける ViewModel の状態遷移をテストしてみます。
今回の例では ViewModel が Webサーバーに HTTP リクエストを発行するとします。
ViewModel は リクエストの発行前/実行中/完了のタイミングで状態遷移します。
この状態遷移に応じて View がインジケーターなどを表示/非表示させるというしくみです。
ViewModel | View | |
---|---|---|
リクエスト発行前 | .Empty |
空のビュー |
リクエスト中 | .InProgress |
インジケーター |
成功 | .Success |
'成功しました'ダイアログ |
ViewModel
は通信クライアント( RestfulClient
)を使って WebサーバーにHTTP リクエストを発行します。
リクエストの結果は Observable のイベントとして受け取ります。
この Observable を返すメソッドとして 通信クライアントには fetch
メソッドがあります。
いっぽう テストコードでは 通信クライアントにモックオブジェクト(MockClient
)を使っています。
fetch
メソッドをモックの Observable を返すメソッドとして実装します。
ViewModel
のテストコードは次のとおりです:
func test_ViewModel() {
class MockClient: Fetchable { // 通信クライアントのモックオブジェクトを定義
let xs: TestableObservable<Int>
init(scheduler: TestScheduler) {
xs = scheduler.createColdObservable([ // モックを作成
next(100, 200) // 時刻 100 で 値 HTTP_OK を出力
])
}
// リクエスト結果の Observable をモックで置き換え
func fetch() -> Observable<Int> { return xs.asObservable() }
}
class MockViewModel: ViewModel {
var scheduler: TestScheduler
init(scheduler: TestScheduler) {
self.scheduler = scheduler
super.init()
}
override var client: Fetchable { return MockClient(scheduler: scheduler) }
}
let scheduler = TestScheduler(initialClock: 0)
let viewModel = MockViewModel(scheduler: scheduler) // テスト対象の ViewModel
let results = scheduler.createObserver(State) // 出力結果を受け取る results
let disposeBag = DisposeBag()
// 時刻 100 で viewModel の state を subscribe
scheduler.scheduleAt(100) {
viewModel.state.asObservable().subscribe(results).addDisposableTo(disposeBag) }
// 200 で viewModel を load する
scheduler.scheduleAt(200) {
_ = viewModel.load().subscribe() }
scheduler.start()
XCTAssertEqual(results.events, [ // 受け取ったイベントを期待値と照合:
next(100, .Empty), // 時刻 100 で 値 .Empty を受け取ること
next(200, .InProgress), // 200 で .InProgress
next(300, .Success) // 300 で .Success
])
}
モックオブジェクトの定義にコードが多くなっているので 読みやすいテストコードとはいえないですが、イベントのタイミングが複雑な Observable を実装するさいは TestScheduler で振る舞いをチェックできるはずです。
ソースコード
動作環境
- Xcode 7.2
- RxSwift 2.0.0-beta.4