Awaitlityの紹介

1前書き

非同期システムに関する一般的な問題は、ビジネスロジックに焦点を当てており、同期、タイムアウト、および同時実行制御に汚染されていない、読み取り可能なテストを書くのが難しいということです。

この記事では、 Awaitility - 非同期システムテスト用の単純なドメイン固有言語(DSL)を提供するライブラリ を見ていきます。

Awaitilityを使用すると、システムからの期待を読みやすいDSLで表現できます。

2依存関係

__pom.xmlにAwaitilityの依存関係を追加する必要があります。

ほとんどのユースケースでは、 awaitility ライブラリで十分です。プロキシベースの条件を使いたい場合は、 awaitility-proxy ライブラリも提供する必要があります。

<dependency>
    <groupId>org.awaitility</groupId>
    <artifactId>awaitility</artifactId>
    <version>3.0.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.awaitility</groupId>
    <artifactId>awaitility-proxy</artifactId>
    <version>3.0.0</version>
    <scope>test</scope>
</dependency>

awaitility の最新バージョンを見つけることができます。 awaitility-proxy Maven Centralのライブラリ。

3非同期サービスの作成

簡単な非同期サービスを書いてテストしましょう。

public class AsyncService {
    private final int DELAY = 1000;
    private final int INIT__DELAY = 2000;

    private AtomicLong value = new AtomicLong(0);
    private Executor executor = Executors.newFixedThreadPool(4);
    private volatile boolean initialized = false;

    void initialize() {
        executor.execute(() -> {
            sleep(INIT__DELAY);
            initialized = true;
        });
    }

    boolean isInitialized() {
        return initialized;
    }

    void addValue(long val) {
        throwIfNotInitialized();
        executor.execute(() -> {
            sleep(DELAY);
            value.addAndGet(val);
        });
    }

    public long getValue() {
        throwIfNotInitialized();
        return value.longValue();
    }

    private void sleep(int delay) {
        try {
            Thread.sleep(delay);
        } catch (InterruptedException e) {
        }
    }

    private void throwIfNotInitialized() {
        if (!initialized) {
            throw new IllegalStateException("Service is not initialized");
        }
    }
}

4 Awaitility によるテスト

それでは、テストクラスを作成しましょう。

public class AsyncServiceTest {
    private AsyncService asyncService;

    @Before
    public void setUp() {
        asyncService = new AsyncService();
    }

   //...
}

私たちのテストは initialize メソッドを呼び出した後、私たちのサービスの初期化が指定されたタイムアウト期間(デフォルト10秒)以内に起こるかどうかチェックします。

このテストケースは単にサービスの初期化状態が変化するのを待つか、または状態の変化が起こらない場合は__ConditionTimeoutExceptionをスローします。

ステータスは Callable によって取得され、指定された初期遅延(デフォルト100ms)の後、定義された間隔(100msデフォルト)でサービスにポーリングします。ここでは、タイムアウト、間隔、および遅延のデフォルト設定を使用しています。

asyncService.initialize();
await()
  .until(asyncService::isInitialized);

ここでは、 await - Awaitility クラスの静的メソッドの1つを使用します。 ConditionFactory クラスのインスタンスを返します。読みやすさを増すために given のような他の方法を使うこともできます。

デフォルトのタイミングパラメータは、 Awaitility クラスの静的メソッドを使用して変更できます。

Awaitility.setDefaultPollInterval(10, TimeUnit.MILLISECONDS);
Awaitility.setDefaultPollDelay(Duration.ZERO);
Awaitility.setDefaultTimeout(Duration.ONE__MINUTE);

ここでは、最も頻繁に使用される期間に有用な定数を提供する Duration クラスの使用方法を見ることができます。

  • それぞれの await 呼び出しに** カスタムタイミング値を提供することもできます。ここでは、初期化が最大で5秒後、少なくとも100ms後にポーリング間隔100msで行われると予想します。

asyncService.initialize();
await()
    .atLeast(Duration.ONE__HUNDRED__MILLISECONDS)
    .atMost(Duration.FIVE__SECONDS)
  .with()
    .pollInterval(Duration.ONE__HUNDRED__MILLISECONDS)
    .until(asyncService::isInitialized);

ConditionFactory には、 with then and givenなどの追加のメソッドが含まれていることを言及する価値があります。これらのメソッドは何もしないで、単に this__を返しますが、テスト条件の読みやすさを向上させるのに役立ちます。

5 Matcherを使う

待ち時間は式の結果をチェックするための hamcrest matcherの使用も可能にします。たとえば、 addValue メソッドを呼び出した後で、 long 値が予想どおりに変更されていることを確認できます。

asyncService.initialize();
await()
  .until(asyncService::isInitialized);
long value = 5;
asyncService.addValue(value);
await()
  .until(asyncService::getValue, equalTo(value));

この例では、最初の await 呼び出しを使用して、サービスが初期化されるまで待機しています。そうでなければ、 getValue メソッドは IllegalStateException をスローします。

6. 例外を無視する

場合によっては、非同期ジョブが完了する前にメソッドが例外をスローする状況があります。私たちのサービスでは、サービスが初期化される前に getValue メソッドを呼び出すことができます。

待機はテストに失敗することなくこの例外を無視する可能性を提供します。

たとえば、 IllegalStateException を無視して、 getValue の結果が初期化直後にゼロに等しいことを確認しましょう。

asyncService.initialize();
given().ignoreException(IllegalStateException.class)
  .await().atMost(Duration.FIVE__SECONDS)
  .atLeast(Duration.FIVE__HUNDRED__MILLISECONDS)
  .until(asyncService::getValue, equalTo(0L));

7. プロキシを使用する

セクション2で説明したように、プロキシベースの条件を使用するには、 awaitility-proxy を含める必要があります。プロキシの考え方は、 Callable またはλ式を実装せずに、条件に対する実際のメソッド呼び出しを提供することです。

AsyncService が初期化されていることを確認するには、 AwaitilityClassProxy.to 静的メソッドを使用します。

asyncService.initialize();
await()
  .untilCall(to(asyncService).isInitialized(), equalTo(true));

8フィールドへのアクセス

待機はプライベートフィールドにアクセスしてアサーションを実行することもできます。

次の例では、サービスの初期化ステータスを取得するための別の方法を見ることができます。

asyncService.initialize();
await()
  .until(fieldIn(asyncService)
  .ofType(boolean.class)
  .andWithName("initialized"), equalTo(true));

9結論

このクイックチュートリアルでは、Awaitilityライブラリを紹介し、非同期システムをテストするための基本的なDSLについて理解し、実際のプロジェクトでライブラリを柔軟かつ使いやすくする高度な機能について説明しました。

いつものように、すべてのコード例はhttps://github.com/eugenp/tutorials/tree/master/libraries[on Github]から入手できます。