34
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

RxSwift の TestScheduler で Observable をテストする

Last updated at Posted at 2015-12-20

(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

参考資料

34
36
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
34
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?