Dealing with virtual time

Despite the fact that the essence of testing is in covering business logic, there is another very important part that should be given thought as well. To understand this characteristic, we should consider the following example of the code first:

public Flux<String> sendWithInterval() {
   return Flux.interval(Duration.ofMinutes(1))
      .zipWith(Flux.just("a", "b", "c"))
      .map(Tuple2::getT2);
}

This example shows a naive way of publishing events with a specific interval. In a real-world scenario, behind the same API, there might be a more complex mechanism hidden that involves long delays, timeouts, and event intervals. To verify such code using StepVerifier, we might end up with the following test case:

StepVerifier
.create(sendWithInterval()) .expectSubscription() .expectNext("a", "b", "c") .expectComplete() .verify();

The proceeding test will be passed toward our previous implementation of sendWithInterval(), which is something that we really want to achieve. However, there is a problem with this test. If we run it a few times, we will find that the average duration of the test is a bit more than three minutes. This occurs because the sendWithInterval() method produces three events with a delay of one minute before each element. In cases in which the interval or schedule time will be hours or days, verification of the system may take a huge amount of time, which is unacceptable when it comes to continuous integration nowadays. To solve this problem, the Reactor Test module offers the ability to replace real time with virtual time, as shown in the following code:

StepVerifier.withVirtualTime(() -> sendWithInterval())
// scenario verification ...

When using the .withVirtualTime() builder method, we explicitly replace every Scheduler in the Reactor with reactor.test.scheduler.VirtualTimeScheduler. In turn, such a replacement means that Flux.interval will be run on that Scheduler as well. Consequently, all control of the time may be done using VirtualTimeScheduler#advanceTimeBy, as shown in the following code:

StepVerifier
.withVirtualTime(() -> sendWithInterval()) .expectSubscription() .then(() -> VirtualTimeScheduler .get() .advanceTimeBy(Duration.ofMinutes(3)) ) .expectNext("a", "b", "c") .expectComplete() .verify();

As we might notice, in the preceding example we use .then() in combination with the VirtualTimeScheduler API to advance the time by a certain amount. If we run that test, it will take a few milliseconds instead of minutes! This result is much better, since now our test behaves regardless of the actual time intervals with which the data is produced. Finally, to make our test clean, we may replace the combination of .then() and VirtualTimeScheduler with .thenAwait(), which behaves identically.

Be aware that in cases in which StepVerifier does not advance time enough, the test may hang forever.

To limit the time spent on the verification scenario, the .verify(Duration t) overload may be used. This will throw AssertionError when the test fails to verify during the allowed duration. Furthermore, the .verify() method returns how long the verification process actually took. The following code depicts such a use case:

Duration took = StepVerifier
.withVirtualTime(() -> sendWithInterval()) .expectSubscription() .thenAwait(Duration.ofMinutes(3)) .expectNext("a", "b", "c") .expectComplete() .verify();

System.out.println("Verification took: " + took);

In cases in which it is important to check that there were no events during the specified waiting time, there is an additional API method called .expectNoEvents(). Using this method, we may check that events are produced using the specified interval, as follows:

StepVerifier
.withVirtualTime(() -> sendWithInterval()) .expectSubscription() .expectNoEvent(Duration.ofMinutes(1)) .expectNext("a") .expectNoEvent(Duration.ofMinutes(1)) .expectNext("b") .expectNoEvent(Duration.ofMinutes(1)) .expectNext("c") .expectComplete() .verify();

From the preceding examples, we might learn a list of techniques that help us make our test fast.

It might be noted that there is an additional overload of the .thenAwait() method with no arguments. The main idea behind this method is the triggering of any tasks that have not yet been executed and that are scheduled to be executed at or before the current virtual time. For example, to receive the first scheduled event in the next set up, Flux.interval(Duration.ofMillis(0), Duration.ofMillis(1000)) will require an additional call of .thenAwait(), as shown in the following code:

StepVerifier
.withVirtualTime(() -> Flux.interval(Duration.ofMillis(0), Duration.ofMillis(1000)) .zipWith(Flux.just("a", "b", "c")) .map(Tuple2::getT2) ) .expectSubscription() .thenAwait() .expectNext("a") .expectNoEvent(Duration.ofMillis(1000)) .expectNext("b") .expectNoEvent(Duration.ofMillis(1000)) .expectNext("c") .expectComplete() .verify();

Without .thenAwait()the test will hang forever.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.30.236