StepVerifierとTestPublisherを使って反応型ストリームをテストする

StepVerifierとTestPublisherを使用したリアクティブストリームのテスト

1. 概要

このチュートリアルでは、StepVerifierTestPublisherを使用してreactive streamsをテストする方法を詳しく見ていきます。

一連の原子炉運転を含むSpring Reactorアプリケーションに基づいて調査を行います。

2. Mavenの依存関係

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

the reactor-test dependencyを追加することで、これらを取得できます。


    io.projectreactor
    reactor-test
    test
    3.2.3.RELEASE

3. StepVerifier

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

  • StepVerifierを使用して段階的なテストを作成する

  • ダウンストリーム演算子をテストするためにTestPublisher を使用して事前定義されたデータを生成する

リアクティブストリームをテストする最も一般的なケースは、コードでパブリッシャー(Flux またはMono)が定義されている場合です。 We want to know how it behaves when someone subscribes. 

StepVerifier APIを使用すると、公開された要素の期待値をwhat elements we expect and what happens when our stream completesで定義できます。

まず、いくつかのオペレーターでパブリッシャーを作成しましょう。

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

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

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

3.1. 段階的なシナリオ

それでは、source StepVerifierin order to test what will happen when someone subscribesでテストしてみましょう。

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

まず、create メソッドを使用してStepVerifier builderを作成します。

次に、テスト中のFlux sourceをラップします。 最初の信号はexpectNext(T element), で検証されますが、実際にはwe can pass any number of elements to expectNext.

expectNextMatches を使用して、よりカスタムな一致のためにPredicate<T> を提供することもできます。

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

そして最後に、we use verify() to trigger our test

3.2. StepVerifierの例外

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

We’ll have this Mono terminate immediately with an error when subscribed to

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

ここで、4つのすべての要素の後、we expect our stream to terminate with an exception

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

We can use only one method to verify exceptions.OnErrorシグナルは、サブスクライバーにthe publisher is closed with an error state. Therefore, we can’t add more expectations afterwardを通知します。

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

  • expectError()  –あらゆる種類のエラーが予想されます

  • expectError(Class<? extends Throwable> clazz) – は特定のタイプのエラーを予期します

  • expectErrorMessage(String errorMessage) – は特定のメッセージを含むエラーを予期します

  • expectErrorMatches(Predicate<Throwable> predicate)  –指定された述語に一致するエラーを予期します

  • expectErrorSatisfies(Consumer<Throwable> assertionConsumer)  –カスタムアサーションを実行するためにThrowable を消費します

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

サイト運営者が時間ベースである場合があります。

たとえば、実際のアプリケーションでwe have a one-day delay between eventsと仮定します。 さて、明らかに、このような遅延で予想される動作を検証するために、テストを1日中実行することは望ましくありません。

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

We create a builder by calling withVirtualTime.Note that this method doesn’t take Flux as input.代わりに、Supplierが必要です。これにより、スケジューラーをセットアップした後、テストされたFlux のインスタンスが遅延して作成されます。

イベント間の予想される遅延をテストする方法を示すために、2秒間実行される1秒間隔のFlux を作成しましょう。 If the timer runs correctly, we should only get two elements:

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

コード内でFlux earlierをインスタンス化してから、Supplier がこの変数を返すことは避けてください。 代わりに、we should always instantiate Flux inside the lambda.

時間を処理する2つの主要な予想方法があります。

  • thenAwait(Duration duration) –は、ステップの評価を一時停止します。この間に新しいイベントが発生する可能性があります

  • durationの間にイベントが発生すると、expectNoEvent(Duration duration) – は失敗します。シーケンスは指定されたdurationで渡されます

最初のシグナルはサブスクリプションイベントであるため、every expectNoEvent(Duration duration) should be preceded with *expectSubscription()*.であることに注意してください。

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

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

ただし、sometimes we need to verify additional state after our whole scenario played out successfully.

カスタムパブリッシャーを作成しましょう。 It will emit a few elements, then complete, pause, and emit one more element, which we’ll drop

Flux source = Flux.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 onを返し、これにアサーションを追加できます。

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

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

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

たとえば、テストする特定の状況がある場合があります。

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

どちらの場合も、TestPublisher<T>を使用できます。allows us to programmatically trigger miscellaneous signals:

  • next(T value)またはnext(T value, T rest) – は、1つ以上のシグナルをサブスクライバーに送信します

  • emit(T value) – sはnext(T) と同じですが、後でcomplete()を呼び出します

  • complete()complete信号でソースを終了します

  • error(Throwable tr) – はソースをエラーで終了します

  • TestPublisher Fluxにラップするためのflux() – 便利なメソッド

  • mono()  –同じflux() ですが、Monoにラップします

4.1. TestPublisherの作成

いくつかの信号を発し、例外を除いて終了する単純なTestPublisher を作成しましょう。

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

4.2. TestPublisherの動作

前に述べたように、a finely chosen signal that closely matches to a particular situation.をトリガーしたい場合があります

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

まず、コンストラクタパラメータとしてFlux<String> を使用して操作getUpperCase()を実行するクラスを作成しましょう。

class UppercaseConverter {
    private final Flux source;

    UppercaseConverter(Flux source) {
        this.source = source;
    }

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

UppercaseConverter が複雑なロジックと演算子を含むクラスであり、source publisherから非常に特定のデータを提供する必要があるとします。

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

final TestPublisher 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 constructorパラメーターにテストFlux publisherを作成します。 次に、TestPublisherは3つの要素を出力し、完了します。

4.3. 不正な動作TestPublisher

一方、we can create a misbehaving TestPublisher with the createNonCompliant factory method.コンストラクターにTestPublisher.Violation.から1つの列挙値を渡す必要があります。これらの値は、パブリッシャーが仕様のどの部分を見落とす可能性があるかを指定します。

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

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

ALLOW_NULL,に加えて、TestPublisher.Violation toを使用することもできます。

  • REQUEST_OVERFLOW –リクエスト数が不十分な場合に、IllegalStateExceptionをスローせずにnext() を呼び出すことができます

  • CLEANUP_ON_TERMINATE – は、終了信号を連続して複数回送信できるようにします

  • DEFER_CANCELLATION –を使用すると、キャンセルシグナルを無視して、要素の放出を続行できます

5. 結論

この記事では、we discussed various ways of testing reactive streams from the Spring Reactor project.

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

いつものように、すべての例の実装はGithub projectにあります。