1. 概要

このチュートリアルでは、リアクティブストリームStepVerifierおよびTestPublisherでテストする方法を詳しく見ていきます。

調査は、reactor操作のチェーンを含む SpringReactorアプリケーションに基づいて行います。

2. Mavenの依存関係

Spring Reactorには、リアクティブストリームをテストするためのいくつかのクラスが付属しています。

これらは、リアクターテストの依存関係を追加することで取得できます。

<dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-test</artifactId>
    <scope>test</scope>
    <version>3.2.3.RELEASE</version>
</dependency>

3. StepVerifier

一般に、reactor-testには2つの主な用途があります。

  • StepVerifierを使用して段階的なテストを作成する
  • TestPublisher を使用して事前定義されたデータを生成し、ダウンストリームオペレーターをテストします

リアクティブストリームをテストする最も一般的なケースは、コードでパブリッシャー(FluxまたはMono)が定義されている場合です。 誰かがサブスクライブしたときの動作を知りたいのです。 

StepVerifier APIを使用すると、期待する要素と、ストリームが完了したときに何が起こるかの観点から、公開された要素の期待を定義できます。

まず、いくつかの演算子を使用してパブリッシャーを作成しましょう。

Flux.just(T要素)を使用します。このメソッドは、指定された要素を放出して完了するフラックスを作成します。

高度な演算子はこの記事の範囲を超えているため、大文字にマップされた4文字の名前のみを出力する単純なパブリッシャーを作成します。

Flux<String> source = Flux.just("John", "Monica", "Mark", "Cloe", "Frank", "Casper", "Olivia", "Emily", "Cate")
  .filter(name -> name.length() == 4)
  .map(String::toUpperCase);

3.1. ステップバイステップのシナリオ

それでは、ソース StepVerifier でテストして、誰かがサブスクライブしたときに何が起こるかをテストしましょう

StepVerifier
  .create(source)
  .expectNext("JOHN")
  .expectNextMatches(name -> name.startsWith("MA"))
  .expectNext("CLOE", "CATE")
  .expectComplete()
  .verify();

まず、createメソッドを使用してStepVerifierビルダーを作成します。

次に、テスト中のFluxソースをラップします。 最初の信号はexpectNext(T element)、で検証されますが、実際には、expectNextに任意の数の要素を渡すことができます。

使用することもできます expectedNextMatches と提供します述語よりカスタムマッチのために。

最後の期待として、ストリームが完了することを期待しています。

そして最後に、 verify()を使用してテストをトリガーします。

3.2. StepVerifierの例外

それでは、FluxパブリッシャーをMonoと連結しましょう。

にサブスクライブすると、このMonoはエラーですぐに終了します。

Flux<String> error = source.concatWith(
  Mono.error(new IllegalArgumentException("Our message"))
);

ここで、4つのすべての要素の後、ストリームが例外で終了することを期待しています。

StepVerifier
  .create(error)
  .expectNextCount(4)
  .expectErrorMatches(throwable -> throwable instanceof IllegalArgumentException &&
    throwable.getMessage().equals("Our message")
  ).verify();

例外を検証するために使用できる方法は1つだけです。 The OnError 信号は加入者に次のことを通知しますパブリッシャーはエラー状態で閉じられます。 したがって、後で期待を追加することはできません

例外のタイプとメッセージを一度にチェックする必要がない場合は、専用の方法の1つを使用できます。

  • expectedError() –あらゆる種類のエラーを予期します
  • expectError(Class <? Throwableを拡張> クラズ )– 特定のタイプのエラーが予想されます
  • expectedErrorMessage(String errorMessage)–特定のメッセージを含むエラーを予期します
  • expectErrorMatches(述語述語) –指定された述語に一致するエラーを予期します
  • expectErrorSatisfies(Consumer assertionConsumer) –消費するスロー可能カスタムアサーションを行うために

3.3. 時間ベースのパブリッシャーのテスト

時々私たちの出版社は時間ベースです。

たとえば、実際のアプリケーションで、イベント間に1日の遅延があるとします。 さて、明らかに、このような遅延で予想される動作を検証するために、テストを1日中実行することは望ましくありません。

StepVerifier.withVirtualTime ビルダーは、長時間実行されるテストを回避するように設計されています。

withVirtualTimeを呼び出してビルダーを作成します。このメソッドは入力としてFluxを受け取らないことに注意してください。代わりに、サプライヤーを取り、インスタンスを遅延作成しますスケジューラーをセットアップした後のテストされたフラックスの。

イベント間の予想される遅延をテストする方法を示すために、2秒間実行される1秒間隔のフラックスを作成してみましょう。 タイマーが正しく実行されている場合、取得できる要素は2つだけです。

StepVerifier
  .withVirtualTime(() -> Flux.interval(Duration.ofSeconds(1)).take(2))
  .expectSubscription()
  .expectNoEvent(Duration.ofSeconds(1))
  .expectNext(0L)
  .thenAwait(Duration.ofSeconds(1))
  .expectNext(1L)
  .verifyComplete();

コードの前半でFluxをインスタンス化してから、Supplierがこの変数を返すことは避けてください。 代わりに、常にラムダ内でFluxをインスタンス化する必要があります。

時間に対処する2つの主要な期待方法があります。

  • thenAwait(Duration duration)–はステップの評価を一時停止します。 この間に新しいイベントが発生する可能性があります
  • expectedNoEvent(Duration duration)– duration 中にイベントが発生すると、は失敗します。 シーケンスは、指定された期間で通過します

最初のシグナルはサブスクリプションイベントであるため、すべてのexpectNoEvent(期間)の前に expectedSubscription()。を付ける必要があることに注意してください。

3.4. StepVerifierを使用した実行後のアサーション

したがって、これまで見てきたように、私たちの期待を段階的に説明するのは簡単です。

ただし、シナリオ全体が正常に実行された後、追加の状態を確認する必要がある場合があります。

カスタムパブリッシャーを作成しましょう。 いくつかの要素を放出し、次に完了して一時停止し、もう1つの要素を放出します。これをドロップします

Flux<Integer> source = Flux.<Integer>create(emitter -> {
    emitter.next(1);
    emitter.next(2);
    emitter.next(3);
    emitter.complete();
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    emitter.next(4);
}).filter(number -> number % 2 == 0);

最初にemitter.completeを呼び出したため、2を放出すると予想されますが、4をドロップします。

それでは、を使用してこの動作を確認しましょう verifyThenAssertThat。 このメソッドは StepVerifier.Assertions アサーションを追加できる場所:

@Test
public void droppedElements() {
    StepVerifier.create(source)
      .expectNext(2)
      .expectComplete()
      .verifyThenAssertThat()
      .hasDropped(4)
      .tookLessThan(Duration.ofMillis(1050));
}

4. TestPublisherを使用したデータの生成

場合によっては、選択した信号をトリガーするために特別なデータが必要になることがあります。

たとえば、テストしたい非常に特殊な状況がある場合があります。

または、独自の演算子を実装して、その動作をテストすることもできます。

どちらの場合も、 TestPublisher 、 どれのプログラムでその他の信号をトリガーできます。

  • next(T value)または next(T value、T rest)–1つ以上の信号をサブスクライバーに送信します
  • emit(T value)– next(T)と同じですが、後で complete()を呼び出します
  • complete()complete信号でソースを終了します
  • error(Throwable tr)–エラーでソースを終了します
  • flux()– TestPublisherFluxにラップするための便利なメソッド
  • mono() –同じ flux()ですが、Monoにラップします

4.1. TestPublisherの作成

いくつかのシグナルを送信し、例外を除いて終了する単純なTestPublisherを作成しましょう。

TestPublisher
  .<String>create()
  .next("First", "Second", "Third")
  .error(new RuntimeException("Message"));

4.2. TestPublisherの動作

先に述べたように、特定の状況に厳密に一致する細かく選択された信号をトリガーしたい場合があります。

さて、この場合、データのソースを完全に熟知していることが特に重要です。 これを実現するために、TestPublisherに再び頼ることができます。

まず、を使用するクラスを作成しましょうフラックス操作を実行するためのコンストラクターパラメーターとして getUpperCase()

class UppercaseConverter {
    private final Flux<String> source;

    UppercaseConverter(Flux<String> source) {
        this.source = source;
    }

    Flux<String> getUpperCase() {
        return source
          .map(String::toUpperCase);
    }   
}

UppercaseConverter が複雑なロジックと演算子を持つクラスであり、ソースパブリッシャーから非常に特定のデータを提供する必要があるとします。

これは、 TestPublisher:で簡単に実現できます。

final TestPublisher<String> testPublisher = TestPublisher.create();

UppercaseConverter uppercaseConverter = new UppercaseConverter(testPublisher.flux());

StepVerifier.create(uppercaseConverter.getUpperCase())
  .then(() -> testPublisher.emit("aA", "bb", "ccc"))
  .expectNext("AA", "BB", "CCC")
  .verifyComplete();

この例では、UppercaseConverterコンストラクターパラメーターでテストFluxパブリッシャーを作成します。 次に、 TestPublisher は3つの要素を発行し、完了します。

4.3. 誤動作TestPublisher

一方、 createNonCompliantファクトリメソッドを使用して、動作に問題のあるTestPublisherを作成できます。コンストラクターにTestPublisher.Violationから1つの列挙値を渡す必要があります。これらの値は、パブリッシャーが見落とす可能性のある仕様。

null要素に対してNullPointerExceptionをスローしないTestPublisherを見てみましょう。

TestPublisher
  .createNoncompliant(TestPublisher.Violation.ALLOW_NULL)
  .emit("1", "2", null, "3");

ALLOW_NULL、に加えて、TestPublisher.Violationを使用して次のことを行うこともできます。

  • REQUEST_OVERFLOW –リクエストの数が不十分な場合に、 IllegalStateException をスローせずに、 next()を呼び出すことができます
  • CLEANUP_ON_TERMINATE –任意の終了信号を連続して複数回送信できます
  • DEFER_CANCELLATION – を使用すると、キャンセル信号を無視して、要素の放出を続行できます

5. 結論

この記事では、SpringReactorプロジェクトからのリアクティブストリームをテストするさまざまな方法について説明しました。

最初に、StepVerifierを使用してパブリッシャーをテストする方法を確認しました。 次に、 TestPublisherの使用方法を確認しました。同様に、TestPublisherの誤動作を操作する方法を確認しました。

いつものように、すべての例の実装はGithubプロジェクトにあります。