Menguji Aliran Reaktif Menggunakan StepVerifier dan TestPublisher

1. Gambaran keseluruhan

Dalam tutorial ini, kita akan melihat secara dekat pengujian aliran reaktif dengan StepVerifier dan TestPublisher .

Kami akan mendasarkan penyelidikan kami pada aplikasi Spring Reactor yang mengandungi rangkaian operasi reaktor.

2. Pergantungan Maven

Spring Reactor dilengkapi dengan beberapa kelas untuk menguji aliran reaktif.

Kita boleh mendapatkannya dengan menambahkan kebergantungan ujian reaktor :

 io.projectreactor reactor-test test     3.2.3.RELEASE 

3. StepVerifier

Secara amnya, ujian reaktor mempunyai dua kegunaan utama:

  • membuat ujian langkah demi langkah dengan StepVerifier
  • menghasilkan data yang telah ditentukan dengan TestPublisher untuk menguji pengendali hiliran

Kes yang paling biasa dalam menguji aliran reaktif adalah ketika kita mempunyai penerbit ( Flux atau Mono ) yang ditentukan dalam kod kita. Kami ingin mengetahui bagaimana tingkah lakunya ketika seseorang melanggan.

Dengan StepVerifier API, kami dapat menentukan jangkaan elemen terbitan kami dari segi elemen apa yang kami harapkan dan apa yang berlaku apabila aliran kami selesai .

Pertama sekali, mari buat penerbit dengan beberapa pengendali.

Kami akan menggunakan Flux.just (elemen T). Kaedah ini akan membuat Flux yang memancarkan elemen yang diberikan dan kemudian selesai.

Oleh kerana pengendali lanjutan berada di luar ruang lingkup artikel ini, kami hanya akan membuat penerbit ringkas yang hanya mengeluarkan nama empat huruf yang dipetakan ke huruf besar:

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

3.1. Senario Langkah demi Langkah

Sekarang, mari kita uji sumber kami dengan StepVerifier untuk menguji apa yang akan berlaku apabila seseorang melanggan :

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

Pertama, kita membuat pembangun StepVerifier dengan kaedah buat .

Seterusnya, kami membungkus sumber Flux kami , yang sedang dalam ujian. Isyarat pertama disahkan dengan expectNext (elemen T), tetapi sebenarnya, kita dapat meneruskan sebilangan elemen untuk diharapkanNext .

Kami juga dapat menggunakan expectNextMatches dan menyediakan Predicate untuk pertandingan yang lebih khusus.

Untuk jangkaan terakhir kami, kami menjangkakan aliran kami akan selesai.

Dan akhirnya, kami menggunakan verifikasi () untuk mencetuskan ujian kami .

3.2. Pengecualian dalam StepVerifier

Sekarang, mari gabungkan penerbit Flux kami dengan Mono .

Kami akan menghentikan Mono ini dengan kesalahan semasa melanggan :

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

Sekarang, setelah empat elemen, kami menjangkakan aliran kami akan berakhir dengan pengecualian :

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

Kami hanya boleh menggunakan satu kaedah untuk mengesahkan pengecualian. The OnError isyarat memberitahu pelanggan bahawa penerbit ditutup dengan keadaan ralat. Oleh itu, kami tidak dapat menambahkan lebih banyak jangkaan selepas itu .

Sekiranya tidak perlu memeriksa jenis dan mesej pengecualian sekaligus, maka kita dapat menggunakan salah satu kaedah khusus:

  • expectError () - menjangkakan sebarang jenis kesalahan
  • expectError (Class clazz ) - menjangkakan ralat jenis tertentu
  • expectErrorMessage (String errorMessage) - mengharapkan ralat mempunyai mesej tertentu
  • expectErrorMatches (Predikat predikat) - mengharapkan ralat yang sepadan dengan predikat tertentu
  • expectErrorSatisfies (Consert assertionConsumer) - menggunakan Throwable untuk melakukan penegasan tersuai

3.3. Menguji Penerbit Berasaskan Masa

Kadang kala penerbit kami berdasarkan masa.

Sebagai contoh, anggaplah bahawa dalam aplikasi kehidupan sebenar, kita mempunyai kelewatan satu hari antara acara . Sekarang, jelas, kami tidak mahu ujian kami berjalan sepanjang hari untuk mengesahkan tingkah laku yang diharapkan dengan kelewatan seperti itu.

StepVerifier.withVirtualTime builder direka untuk mengelakkan ujian yang lama.

Kami membuat pembangun dengan memanggil denganVirtualTime . Perhatikan bahawa kaedah ini tidak menggunakan Fluxsebagai input. Sebaliknya, ia memerlukan Pembekal , yang dengan malas membuat contoh Flux yang diuji setelah penjadualnya disiapkan.

Untuk menunjukkan bagaimana kita dapat menguji jangkaan penangguhan antara acara, mari buat Flux dengan selang satu saat yang berlangsung selama dua saat. Sekiranya pemasa berjalan dengan betul, kita hanya perlu mendapatkan dua elemen:

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

Perhatikan bahawa kita harus mengelakkan memberi contoh Flux sebelumnya dalam kod dan kemudian meminta Pembekal mengembalikan pemboleh ubah ini. Sebagai gantinya, kita harus selalu menunjukkan Flux di dalam lambda.

Terdapat dua kaedah jangkaan utama yang menangani masa:

  • thenAwait(Duration duration) – pauses the evaluation of the steps; new events may occur during this time
  • expectNoEvent(Duration duration) – fails when any event appears during the duration; the sequence will pass with a given duration

Please notice that the first signal is the subscription event, so every expectNoEvent(Duration duration) should be preceded with expectSubscription().

3.4. Post-Execution Assertions with StepVerifier

So, as we've seen, it's straightforward to describe our expectations step-by-step.

However, sometimes we need to verify additional state after our whole scenario played out successfully.

Let's create a custom publisher. 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);

We expect that it will emit a 2, but drop a 4, since we called emitter.complete first.

So, let's verify this behavior by using verifyThenAssertThat. This method returns StepVerifier.Assertions on which we can add our assertions:

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

4. Producing Data with TestPublisher

Sometimes, we might need some special data in order to trigger the chosen signals.

For instance, we may have a very particular situation that we want to test.

Alternatively, we may choose to implement our own operator and want to test how it behaves.

For both cases, we can use TestPublisher, which allows us to programmatically trigger miscellaneous signals:

  • next(T value) or next(T value, T rest) – send one or more signals to subscribers
  • emit(T value) – same as next(T) but invokes complete() afterwards
  • complete() – terminates a source with the complete signal
  • error(Throwable tr) – terminates a source with an error
  • flux() – convenient method to wrap a TestPublisher into Flux
  • mono() – same us flux() but wraps to a Mono

4.1. Creating a TestPublisher

Let's create a simple TestPublisher that emits a few signals and then terminates with an exception:

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

4.2. TestPublisher in Action

As we mentioned earlier, we may sometimes want to trigger a finely chosen signal that closely matches to a particular situation.

Now, it's especially important in this case that we have complete mastery over the source of the data. To achieve this, we can again rely on TestPublisher.

First, let's create a class that uses Flux as the constructor parameter to perform the operation getUpperCase():

class UppercaseConverter { private final Flux source; UppercaseConverter(Flux source) { this.source = source; } Flux getUpperCase() { return source .map(String::toUpperCase); } }

Suppose that UppercaseConverter is our class with complex logic and operators, and we need to supply very particular data from the source publisher.

We can easily achieve this with 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();

In this example, we create a test Flux publisher in the UppercaseConverter constructor parameter. Then, our TestPublisher emits three elements and completes.

4.3. Misbehaving TestPublisher

On the other hand, we can create a misbehaving TestPublisher with the createNonCompliant factory method. We need to pass in the constructor one enum value from TestPublisher.Violation. These values specify which parts of specifications our publisher may overlook.

Let's take a look at a TestPublisher that won't throw a NullPointerException for the null element:

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

In addition to ALLOW_NULL, we can also use TestPublisher.Violation to:

  • REQUEST_OVERFLOW – allows calling next() without throwing an IllegalStateException when there's an insufficient number of requests
  • CLEANUP_ON_TERMINATE – allows sending any termination signal several times in a row
  • DEFER_CANCELLATION – allows us to ignore cancellation signals and continue with emitting elements

5. Conclusion

In this article, we discussed various ways of testing reactive streams from the Spring Reactor project.

First, we saw how to use StepVerifier to test publishers. Then, we saw how to use TestPublisher. Similarly, we saw how to operate with a misbehaving TestPublisher.

Seperti biasa, pelaksanaan semua contoh kami boleh didapati dalam projek Github.