RxSwift の TestScheduler で Observable をテストする

_(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](https://github.com/ReactiveX/RxSwift/blob/e39f5dbce18632bbcbfb11437fdb3b572e376f97/RxTests/RxSwiftTests/Tests/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 の違いについては[別の記事](http://qiita.com/kumapo/items/9cc6727295533709e4b0)をどうぞ。

# 例) 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 で振る舞いをチェックできるはずです。

# ソースコード

- [kumapo/RxSwiftTestSchedulerExample](https://github.com/kumapo/RxSwiftTestSchedulerExample)

# 動作環境

- Xcode 7.2
- RxSwift 2.0.0-beta.4

# 参考資料

- [Intro to Rx](http://www.introtorx.com/content/v1.0.10621.0/16_TestingRx.html#TestingRx)
- [Reactive Programming with RxJS](https://pragprog.com/book/smreactjs/reactive-programming-with-rxjs)
- [Swiftのmanual mocking](http://blog.ishkawa.org/2015/03/20/1426820923/)