[Enhancement Request] Allow `AssertSubscriber` to test for elapsed time with a virtual clock
It would be nice using AssertSubscriber to be able to test that a certain amount of time has elapsed between events. Additionally, it would be nice to be able to replace the clock with a "virtual" clock where you don't actually have to wait said amount of time.
I'll explain using an example from Spring WebFlux (using project reactor) I was trying to replicate in Quarkus using AssertSubscriber on a Multi.
Here's the (Spring WebFlux) code I'm trying to test (its been "dumbed down" for the purposes of this illustration):
@Component
public class Generator implements Supplier<Flux<Integer>> {
private final Random random = new Random();
@Override
public Flux<Integer> get() {
return Flux.interval(Duration.ofSeconds(5))
.onBackpressureDrop()
.map(tick -> this.random.nextInt(100));
}
}
And here's the Spring WebFlux test of this code, using StepVerifier from Project Reactor:
class Tests {
Generator generator = new Generator();
private static final Predicate<Integer> VALUE_TEST =
value -> (value >= 0) && (value < 100);
@Test
public void generatesProperly() {
StepVerifier
.withVirtualTime(() -> this.generator.get().take(2))
.expectSubscription()
.expectNoEvent(Duration.ofSeconds(5))
.expectNextMatches(VALUE_TEST)
.thenAwait(Duration.ofSeconds(5))
.expectNextMatches(VALUE_TEST)
.expectComplete()
.verify(Duration.ofSeconds(15));
}
}
The test uses Project reactor's withVirtualTime to swap out the clock used on the schedulers to a virtual clock.
So in my test case I'm waiting for 2 items to appear in the pipeline, but the clock doesn't actually need to wait for 10 seconds for them to appear.
This test case also allows me to assert that certain durations of time pass between events or items appearing in the stream, which isn't something I can currently do with Mutiny or its testing framework (unless I just don't see it).
The closest I can currently get is something like this (assertThat comes from the assertj-core library):
@ApplicationScoped
public class Generator {
private final Random random = new Random();
public Multi<Integer> generate() {
return Mult.createFrom().ticks().every(Duration.ofSeconds(5))
.onOverflow().drop()
.map(tick -> this.random.nextInt(100));
}
}
class Tests {
Generator generator = new Generator();
private static final Predicate<Integer> VALUE_TEST =
value -> (value >= 0) && (value < 100);
@Test
public void generatesProperly() {
List<Integer> items = this.generator.generate()
.select().first(2)
.subscribe().withSubscriber(AssertSubscriber.create(2))
.assertSubscribed()
.awaitNextItem(Duration.ofSeconds(5))
.awaitNextItem(Duration.ofSeconds(5))
.awaitCompletion(Duration.ofSeconds(15))
.assertCompleted()
.getItems();
assertThat(items)
.hasSize(2)
.allMatch(VALUE_TEST);
}
}
There are a couple of different ways I could have written the assert pipeline, but my point is that there is no way where I can write assertions against elapsed time between items (& before the first item).
It might seem like .awaitNextItem(Duration.ofSeconds(5)) is the same as .thenAwait(Duration.ofSeconds(5)) but they are different semantically - .awaitNextItem(Duration.ofSeconds(5)) waits at most 5 seconds for the next item to appear but will continue if the item appears before the 5 seconds, whereas .thenAwait(Duration.ofSeconds(5)) waits for 5 seconds and if the item appears before the 5 seconds, the test fails.
As a side note as well, I find that this Mutiny test randomly succeeds and fails. I suspect it fails due to the timing. When it fails I get this:
Tests.generatesProperly:26 Expected 1 items, but received a completion event while waiting. Only 0 item(s) have been received.
If I increase .awaitNextItem(Duration.ofSeconds(5)) to .awaitNextItem(Duration.ofSeconds(7)), it pretty much always passes.
With the virtual clock, my test wouldn't actually have to wait the 10-15 seconds for the test to complete.