11
6

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.

この記事は VALU Advent Calendar 12日目の記事です。

こんにちは。Xamarin人材です。最近はAndroidネイティブやっています。
C#だろうとKotlinだろうと、Rxっていいですよね。でも非同期処理のテストって大変そうだったりしませんか?
TestSchedulerってやつがありましてね、これがすごいイイヤツなんですよ。そんな話をします。

サンプルコードはRxJava/RxKotlinですが、Rx.NET等でもほぼ同等の機能はありますので、適宜読み替えていただければと思います。

一定時間ごとにイベントを発火するような処理のテスト

以下のような処理をテストする場合を考えてみましょう。
指定された時間ごとに、計4回文字列を通知する機能です。

fireEventWithInterval.kt
// 指定された時間間隔でイベントを発火する
fun fireEventWithInterval(interval: Long): Observable<String> =
    Observable.interval(interval, TimeUnit.MILLISECONDS)
        .take(4)
        .scan(0) { idx, _ -> idx + 1 }
        .map {
            when (it) {
                0 -> "ひとつめ"
                1 -> "ふたつめ"
                2 -> "みっつめ"
                3 -> "さいご"
                else -> throw Exception()
            }
        }

普通にテストを書いてみる

さて、ちゃんと指定したインターバルでイベントが発火することをテストで保証したいです。
TestSchedulerを利用せずにテストを書くと、このようになるでしょうか。

realTimeTest.kt
    @Test
    fun test_in_real_time() {
        fireEventWithInterval(1000)
            .test()
            .awaitCount(4)
            .assertValueAt(0, "ひとつめ")
            .assertValueAt(1, "ふたつめ")
            .assertValueAt(2, "みっつめ")
            .assertValueAt(3, "さいご")
    }

Observable<T>をテストするために、TestObserverを利用しています。
こちらについては詳しく解説しませんが、テスト用の便利なユーティリティが揃っています。

これで何の問題もなくテストは書けるのですが、1点問題があります。

image.png

実行に3秒かかってしまっているのです。

1秒ごとのインターバルでイベントを発火させているわけですから、当然です。
しかし、これだとCIで日常的に回すには辛いですよね。今後テストが増えるたびに秒単位でテスト所要時間が増えてしまいます。

TestSchedulerで時を司る

そこでTestSchedulerを利用します。

usingTestScheduler.kt
    @Test
    fun test_on_test_scheduler() {
        val testScheduler = TestScheduler()

        // スケジューラーを指定してやる
        val testObserver = fireEventWithInterval(1000, testScheduler)
            .test()

        // 即時1つめのイベントは飛んでくる
        testObserver.assertValueCount(1).assertValue { it == "ひとつめ" }

        // 時間を進める
        testScheduler.advanceTimeBy(1000, TimeUnit.MILLISECONDS)

        // 1秒経過したので2つ目のイベント
        testObserver.assertValueCount(2).assertValueAt(1) { it == "ふたつめ" }

        // テスト開始から3秒経過後に時間を進める
        testScheduler.advanceTimeTo(3000, TimeUnit.MILLISECONDS)

        // 3秒経過したので全てのイベントが飛んでいるはず
        testObserver.assertValueCount(4)
            .assertValueAt(0, "ひとつめ")
            .assertValueAt(1, "ふたつめ")
            .assertValueAt(2, "みっつめ")
            .assertValueAt(3, "さいご")
    }

ご覧の通り、testScheduler.advantTimeXXで時間を操作していることがわかると思います。
任意時間経過後のイベントの発行状態をチェック、ということも柔軟に可能です。

さて、テストの所要時間はどうなったでしょうか。

image.png

はやくなりました。これならCIもどんとこいですね。

1つだけ注意しなければいけないのは、fireEventWithIntervalにschedulerを渡すようにしている点です。

fireEventWithInterval.kt
fun fireEventWithInterval(interval: Long, scheduler: Scheduler): Observable<String> =
    Observable.interval(interval, TimeUnit.MILLISECONDS, scheduler)
        .take(4)
        .scan(0) { idx, _ -> idx + 1 }
        .map {
            when (it) {
                0 -> "ひとつめ"
                1 -> "ふたつめ"
                2 -> "みっつめ"
                3 -> "さいご"
                else -> throw Exception()
            }
        }

Observable.Intervalのインターバルを測るSchedulerとしてTestSchedulerを指定することで、
TestSchedulerが司る時間軸の中で動くようになるんですね。神になった気分ですね。

ちなみに、Observable.IntervalSchedulerを渡さない場合、Schedulers.computation()が利用されるようです。プロダクションではそちらのスケジューラを渡すようにすればよいですね。

おわりに

ReactiveなコードのテストやTestScheduler/TestObserverはまだまだ奥が深いのですが、
今回はさわりとして、ざっくりとした使い方を紹介してみました。

私、過去にTestSchedulerを知らずにカオスなテストコードを書き散らした過去がありますので、
それらを悔い改めるためにこの記事を書きました。同じ過ちを犯す人が少しでも減ることを願います。

明日はVALU iOSアプリを支える、メンション機能にたいへん詳しい @kamexxx1209 さんによるiOS関連トピックのご紹介でーす。

11
6
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
11
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?