RxTestを使ってみる

 さっと動かしてみる

func test_observer() {
    // ①TestSchedulerを生成する (仮想時刻を0で初期化)
    let scheduler = TestScheduler(initialClock: 0)
    // ②TestObserverを生成する
    let observer = scheduler.createObserver(Int.self)
    // ③イベントを流す
    scheduler.scheduleAt(10) { observer.onNext(1) }
    scheduler.scheduleAt(20) { observer.onNext(2) }
    scheduler.scheduleAt(30) { observer.onNext(3) }
    scheduler.scheduleAt(40) { observer.onCompleted() }
    // ④TestSchedulerをスタートさせる
    scheduler.start()
    XCTAssertEqual(observer.events, [
        next(10, 1),
        next(20, 2),
        next(30, 3),
        completed(40)
    ])
}

TestScheduler

最小限以下で大丈夫そう

// 仮想時刻を指定して初期化
let scheduler = TestScheduler(initialClock: 0)

// 任意の型のObserverを生成
let observer = scheduler.createObserver(Int.self)

// Hot or Coldのobservableを生成

scheduler.createColdObservable([
  next(10, 1),
  next(20, 2),
  completed(30)
])

// 任意の時刻にクロージャーの処理を実行
scheduler.scheduleAt(200) {  }

// schedulerを開始
scheduler.start()

HotObservableをテストしてみる

func test_hot_observable() {
    let bag = DisposeBag()
    // ①TestSchedulerを生成する (仮想時刻を0で初期化)
    let scheduler = TestScheduler(initialClock: 0)
    // ②TestObservableを生成する
    let observable: TestableObservable<Int> = scheduler.createHotObservable([
        next(100, 1),
        next(200, 2),
        next(300, 3),
        next(400, 4),
        completed(500)
    ])
    // ③TestObserverを生成する
    let observer: TestableObserver<String> = scheduler.createObserver(String.self)
    // ④TestObserverをTestObservableに購読させる (仮想時刻250にクロージャーの処理を実行)
    scheduler.scheduleAt(250) {
        observable
            .map { String($0) }
            .subscribe(observer)
            .disposed(by: bag)
    }
    // ⑤TestSchedulerをスタートさせる
    scheduler.start()
    // ⑥イベントの検証 (今回はHotなObservableに仮想時刻250で購読したのでそれ以前のイベントは流れてこない)
    let expectedEvents = [
        next(300, "3"),
        next(400, "4"),
        completed(500)
    ]
    XCTAssertEqual(observer.events, expectedEvents)
}

こちらを最新版で動かした。あと若干コメントが間違ってた。

シンプルなViewModelをテストしてみる

動画プレイヤーのようにボタンをタップすると再生・ポーズが切り替わるような画面を想定する。

func test_toggle_playing() {
     let bag = DisposeBag()
     let scheduler = TestScheduler(initialClock: 0)

     // --tap--tap--tap--tap--
     let tapButtonEvent = scheduler.createHotObservable([
         next(10, ()),
         next(20, ()),
         next(30, ()),
         next(40, ())
     ]).asObservable()

     // observer
     let isPlaying = scheduler.createObserver(Bool.self)
     let viewModel = TestViewModel()

     tapButtonEvent
         .bind(to: viewModel.input.tapPlayButton)
         .disposed(by: bag)

     viewModel
         .output
         .isPlaying
         .bind(to: isPlaying)
         .disposed(by: bag)

     scheduler.start()

     XCTAssertEqual(isPlaying.events, [
         next(0, false), // 初期値
         next(10, true),
         next(20, false),
         next(30, true),
         next(40, false),
     ])
 }
  • ボタンタップの入力ストリームと再生中かを表す出力がいい感じにテストできた。

  • ボタンタップのイベントをObservableに抽象化?したので実際にボタンを叩かなくても、イベントを作成すれば良い。

  • またViewModelの出力をうまくViewとbindできていることを前提とすれば、outputをテストできればUITestは省略できそう。

少し大げさだがこの時のViewModel

final class TestViewModel {

    struct Output {
        var isPlaying: Observable<Bool>
    }

    struct Input {
        var tapPlayButton: AnyObserver<Void>
    }

    private let bag = DisposeBag()
    private let isPlaying = BehaviorRelay(value: false)


    let input: Input
    let output: Output

    init() {
        let _tapPlayButton = PublishSubject<Void>()
        input = Input(
            tapPlayButton: _tapPlayButton.asObserver()
        )

        output = Output(
            isPlaying: isPlaying.asObservable()
        )

        _tapPlayButton.subscribe(onNext: { [unowned self] in
            self.isPlaying.accept(!self.isPlaying.value)
        }).disposed(by: bag)
    }
}

参考