失敗は一時の恥

パッケージソフト開発をしているプログラマが気の赴くままに何かを投稿するブログ.

Awaitility で非同期処理のテストをいい感じに書く

概要

Awaitility という,Java で非同期な処理に対するテストを書く際の便利ライブラリがあったのでその紹介です.

マルチスレッドで諸々が背後で動いているときに,テストが期待する状態になるのを待つコードをいい感じに実装できるというもので,Usage の例を挙げると

await().until(newUserIsAdded());

とか

await().atMost(5, SECONDS).until(newUserWasAdded());

みたいな書き方ができるようになります.いい感じっぽいですよね?!

使い方

ドキュメントを軽く読んでみた薄いまとめです.

ライブラリの入れ方

Maven を使っているならこんな感じで依存関係を追加します.

<dependency>
      <groupId>org.awaitility</groupId>
      <artifactId>awaitility</artifactId>
      <version>3.1.2</version>
      <scope>test</scope>
</dependency>

他にも Gradle や SBT の設定例が Getting started に載っています.

使い方

Usage に載っている簡単な例を 3 つほど試してみました.

最も単純な例

何かしらの非同期な処理でユーザーが追加される,というコードをテストするというシチュエーションです.

追加されるユーザーのデータを取得してアサーションをかける前に,こんな感じで待機します:

await().until(newUserIsAdded());

ここで newUserIsAdded() は,ユーザーが増えたことを判定する Callable です:

private Callable<Boolean> newUserIsAdded() {
      return new Callable<Boolean>() {
            public Boolean call() throws Exception {
                  return userRepository.size() == 1;
            }
      };
}

再利用性がより高い例

until() に,真理値を返す Callable を渡すのではなく,値のサプライヤとマッチャを渡すこともできます:

await().until( userRepositorySize(), equalTo(1) );

ここで userRepositorySize() は次のようなサプライヤです:

private Callable<Integer> userRepositorySize() {
      return new Callable<Integer>() {
            public Integer call() throws Exception {
                  return userRepository.size();
            }
      };
}

一方で equalTo()Hamcrest のマッチャです.普段から JUnit のアサートに使う人も多いと思います.

こうしておくことで,期待する状態というのがユーザーが 1 人追加された状態なのか 3 人追加された状態なのかと条件が変わっても,テストコードが書きやすくなりますね.

Proxy を使った条件

自分で都度 Callable を作るのではなく,AwaitilityClassProxy に定義されている to メソッドを使って次のような書き方もできます:

await().untilCall( to(userRepository).size(), equalTo(3) );

この Proxy を使うには,awaitility-proxy という依存関係を別途追加する必要があります.

その他

他にもサンプルがありますが僕が読んで試したのはここまでです. 興味が湧いた人はご自身で Usage あたりを読んでみてください.

仕組み

テストコードをいい感じに書けるわけですが,実施にやっていることは条件式のポーリングのようです.

初期値の説明 をみたところ,とりあえず何も設定せずに await() した場合は次のように動作するようです:

  • ポーリング開始の初期遅延は 100 ミリ秒
  • ポーリングの間隔は 100 ミリ秒
  • ポーリングのタイムアウトは 10 秒

所感

自分も最近非同期なコードに対するテストを書いていて,少なくとも自分の開発機で動かしている分には何の問題もないのですが,他のプロジェクトと同居しているビルドサーバーや,誰かが作業用に作った低スペックな VM だとタイミング問題でテストがこけるという事象がときどき起きてしまっています.

単純に Thread.sleep() を長めに見積もって設定しても無駄な待ち時間でテストやビルドが長引くので,じゃあポーリング的なことをすればいいかといっても適当に書くとコードが汚くなるし,いい感じのライブラリないかな〜と探していたら見つけたのが Awaitility でした.

ということで,ときどき失敗してしまうテストコードの修正にでも近々取り組みたいと思います……