Infinito Nirone 7

白羽の矢を刺すスタイル

RxJava で delay など時間に関するオペレータを使ったときのテスト

例えば、次のコードのような 5秒後に "foo" という String を emit する Observable をかえす関数があるとします。

fun observeFoo(): Observable<String> = Observable.just("foo").delay(5, TimeUnit.SECONDS)

この関数をユニットテストすることを考えます。

RxJava では ObservableSingle などがどんな値を emit したかを検証する目的で TestObserver をつかってユニットテストを書きます。 ObservableSingle などには、この TestObserver に変換する test() メソッドがあるので、このあとに 1つめの値は "foo" である ことを確かめるメソッドをチェインしていきます。

@Test
fun observeFoo_getAfter5s() {
  observeFoo()
    .test()
    .assertValueAt(0) { value ->
      value == "foo"
    }
}

このテストはそのままでは動きません。なぜなら 5秒後 ということを無視しており、すぐさま assertValueAt で値が"foo"かどうか検証してしまうからです。

TestObserver には、値が X 個流れてくるまで待つために awaitCount メソッドが用意してあります。この場合は5秒後に1個値が出るので、次のようにすると正しくテストが動きます。

@Test
fun observeFoo_getAfter5s() {
  observeFoo()
    .test()
    .awaitCount(1) // 値が 1 個くるまで待つ
    .assertValueAt(0) { value ->
      value == "foo"
    }
}

ただし、この方法では、とにかく値が1個くるまでスレッドを止めて待つため、ユニットテストの実行を5秒間止めてしまいます。 同様のテストケースが増えれば増えるほど、勢いよくテストが遅くなるのは明らかです。

delay は ComutationScheduler を使って時間をはかり、指定した時間が経過したら値を emit します。 RxJava ではいろいろな Scheduler をテスト用に上書きするための RxJavaPlugins があり、さらに時間を仮想的に操作するための TestScheduler も用意してあります。 これらを用いて次のように記述すると、5秒間スレッドを止めることなく即座に値の検証が可能になります。

@Test
fun observeFoo_getAfter5s() {
   // delay は標準で computation scheduler を使うので、はじめに computation scheduler を test scheduler に上書きする
  val testScheduler = TestScheduler()
  RxJavaPlugins.setComputationSchedulerHandler { testScheduler }

  // TestObserver を得る
  val testObserver = observeFoo().test()

  // 仮想的に時間を 5 秒すすめる
  testScheduler.advanceTimeBy(5, TimeUnit.SECONDS)

  // "foo" が emit されているので検証ができる
  testObserver.assertValueAt(0) { value ->
    value == "foo"
  }
}

最後に、RxJavaPlugins で上書きしたものは、@After などで RxJavaPlugins.reset() で元に戻しておくことを忘れないでください。