hydrakecat’s blog

Walking like a cat

RxJava のテスト(2): RxJavaHooks, RxAndroidPlugins

これはRxJava Advent Calendar 2016の第14日目の記事です。前回はTestSubscriberやTestSchedulerを使ってRxJavaに関わる単体テストのしかたを説明しました。

f:id:hydrakecat:20161211234713p:plain

この記事では、それらでは足りない場合の奥の手、スケジューラを差し替える方法について説明します。

TestSubscriberやTestSchedulerでは解決しない場合

以下のコードを見てください。これは前回の記事で使用した AsyncService をラップした WrappedService のテストです。このラップが適切かどうかは、措いておきましょう :-)

  @Test public void testWrappedService() {
    final WrappedService service = new WrappedService(new AsyncService());

    service.start();
    assertEquals("success", service.result);
  }

  private static class WrappedService {
    private final AsyncService service;
    private String result;

    public WrappedService(AsyncService service) {
      this.service = service;
    }

    public Subscription start() {
      return service.doSomething().subscribeOn(Schedulers.computation()).subscribe(s -> result = s);
    }
  }

このテストは予想通り失敗します。なぜならタイミングをいっさいコントロールしていないからです。そして、前回見たようなTestSubscriberやTestSchedulerを使ってタイミングをコントロールしたくても、そもそもObservableを返さないので指定しようがありません。この例のように、内部でsubscribeを呼んでいる場合は、スケジューラの指定方法がないので、お手上げになってしまいます。

このような、テスト側からスケジューラをコントロールできない場合の奥の手があります。それはRxJava全体のスケジューラを書き換えてしまう方法です。

RxJava のスケジュールの差し替え

ここからは、1.x 系と 2.x 系でやり方が少しだけ変わります。まずは 1.x の差し替え方法を見てみましょう。

RxJava 1.x 系でのスケジューラの差し替え

RxJava にはデフォルトの挙動を切り替えるためのプラグイン機構があるので、それを使います。古い記事を見ていると RxJavaPlugins クラスを使っているのですが、RxJava 1.1.6 からは RxJavaHooks というクラスのメソッドに集約されたので、そちらを使いましょう。

以下のコードを実行すれば、 Schedulers.computation() のスケジューラを差し替えることができます。フックを設定すると Schedulers.computation() が呼ばれるたびに指定した関数が呼び出され、引数としてデフォルトのスケジューラが渡されるので、好みのスケジューラを返しましょう。

なお、ここでは Schedulers.immediate() を返していますが、前回説明したTestSchedulerを渡せば、好きにスケジューラを操ることができます。

  @Before
  public void setup() {
    RxJavaHooks.setOnComputationScheduler(s -> Schedulers.immediate());
  }

  @After
  public void teardown() {
    RxJavaHooks.reset();
  }

期待通りに動いたでしょうか?これに加えて Schedulers.io()Schedulers.newThread() で返されるスケジューラを差し替えるための RxJavaHooks#setOnIOScheduler()RxJavaHooks#setOnNewThreadScheduler() も用意されています。

1.1.6までは、RxJavaPluginsの制約から、RxJavaPluginsと同名のパッケージのクラスを用意したり、自作のTestRunnerを書かなければならなかったのですが、RxJavaHooksが導入されてからは、そのような面倒な作業は必要なくなりました。

1つだけ注意点があります。この差し替えはグローバルに効いてしまうため、他のテストに影響を与える可能性があります。特に RxJavaPlugins も併用している場合は注意しましょう。RxJavaHooksはデフォルトで何もフックがなければRxJavaPluginsへ委譲します。そしてフックを設定した後でも RxJavaHooks#reset() メソッドを呼べば設定されたフックを破棄してRxJavaPluginsへの委譲が復活します。なお、 RxJavaHooks#clear() を呼んでしまうと、RxJavaPluginsも含めてすべてのフックがクリアされてしまいます。

RxJava 2.x 系でのスケジューラの差し替え

RxJava 2.x 系にも同じプラグイン機構があります。ただし、こちらの名前はRxJavaPluginsです。1.x の古いプラグインクラスと同名です。紛らわしいですね。上記のRxJavaHooksの名前だけを変えたものと思えば良いでしょう。使い方も同様です。

  @Before public void setup() {
    RxJavaPlugins.setComputationSchedulerHandler(s -> Schedulers.trampoline());
  }

  @After public void teardown() {
    RxJavaPlugins.reset();
  }

ここで、RxJava 1.x で使った Schedulers.immediate() を使っていないことに注意しましょう。そうです、RxJava 2.x では Schedulers.immediate() がなくなってしまったのです。その代わりに、上に書いたような trampoline() を使っても良いですし、TestSchedulerを使っても良いでしょう。それ以外の使い方はRxJavaHooksとほぼ同じです。

AndroidSchedulers のスケジューラの差し替え

ここで、すこしRxJava本体からは離れてRxAndroidというライブラリに触れます。というのも、Androidのテストを書いているときに、非常にしばしば、このライブラリの AndroidSchedulers#mainThread() が返すスケジューラを差し替えたくなるからです。

もし、Androidのテストを書いていて、テスト対象のメソッドが内部で AndroidShedulers.mainThread() を呼んでいた場合、特になにもしていないなら、次のようなエラーが出るはずです。

java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. See http://g.co/androidstudio/not-mocked for details.

これは getMainLooper() がモックされていないというエラーです。ここでRobolectricを使ってAndroidのLooperをモックしても良いのですが、もっと簡単な方法があります。

RxJavaHooksと同じようなプラグインRxAndroidPluginsというものが用意されているのです。

使い方を見てみましょう。

  @Before public void setup() {
    RxAndroidPlugins.getInstance().registerSchedulersHook(new RxAndroidSchedulersHook() {
      @Override
      public Scheduler getMainThreadScheduler() {
        return Schedulers.immediate();
      }
    });
  }

  @After public void teardown() {
    RxAndroidPlugins.getInstance().reset();
  }

RxAndroidSchedulerHook#getMainThreadScheduler()AndroidSchedulers.mainThread() が呼ばれたタイミングで呼ばれます。そこで差し込みたいスケジューラを返しましょう。

なお、すでにフックがセットされている状態で、この registerSchedulerHook() メソッドを呼ぶとエラーになります。必ずセットする前に reset() を呼ぶか、この registerSchedulerHook() メソッドが1度しか呼ばれないようにしましょう。

まとめ

前回と今回とで、RxJavaを使ったコードをどうテストするかを見てきました。

実は、この一連の記事を書いたきっかけは、DevFest Tokyoで自分が「初心者のためのRxJava」というセッションを行った際に受けた「RxJavaのテストはどうしたら良いですか?」という質問でした。そのときはここに書いてあるようなことを口頭で答えたのですが、どこかにまとめておけば、そのリンクを示すだけで済んだのに、と思い、ここに書いた次第です。同じような悩みを抱えている人の助けになれば幸いです。