reactor-core icon indicating copy to clipboard operation
reactor-core copied to clipboard

Added overloaded expectNextMatch to match multiple elements

Open mipo256 opened this issue 8 months ago • 3 comments

Currently, the StepVerifier has an expectNextMatch method. It is convenient to use in tests when we have an object that is too complex to instantiate, and we do not care about equals(), rather about some very specific details.

Unfortunately, expectNextMatch verifies only the single oNext(). It would be great to have an overloaded variant, something like expectNextMatch(Predicate predicate, Long n). This would check the next n onNext elements against the predicate.

If accepted, I can assist with implementation.

mipo256 avatar Apr 27 '25 06:04 mipo256

Hey, @mipo256. Thank you for the suggestion. I am wondering whether you tried recordWith along with consumeRecordedWith - I think it would satisfy your needs. Is there something you can't express with this or a related combination?

There is also thenConsumeWhile which allows you to use the overload that takes a Consumer - perhaps it is even more helpful in case you don't want to aggregate the signals into a collection?

chemicL avatar May 05 '25 10:05 chemicL

I am wondering whether you tried recordWith along with consumeRecordedWith - I think it would satisfy your needs. Is there something you can't express with this or a related combination?

That is not a very convenient approach. The concern is mostly not about the space occupied on the heap, these are just tests at the end of the day.

The problem is that I just want to declaratively specify that I want precisely N next elements to match the predicate. And to do that I need to record all of them into some collection, and then iterate over this collection and check manually if every element matches the condition. That is not very appealing, to be honest.

There is also thenConsumeWhile which allows you to use the overload that takes a Consumer - perhaps it is even more helpful in case you don't want to aggregate the signals into a collection?

That one is better, I did not know about it, so thank you. However, it does not really meet the requirement. The problem is that thenConsumeWhile() can overconsume in my case. Let me explain.

Let's say that I have a stream of 10 elements. And I want precisely 9 of the first elements to match the condition, and 10th element to be precisely equals() to something. The last 10th element is special, as it is the final one, and I want to check it a bit differently.

The problem is that if 10th element would also match the condition of the first 9th then in this case thenConsumeWhile() would consume all 10. It is not really a very big deal, but in this case I cannot control the while.

I hope the concern is clear.

mipo256 avatar May 05 '25 16:05 mipo256

I think the question with your proposal is - what happens after N? We do have exactly a mechanism for this scenario and it's the recordWith pattern. Please have a look:

		int limit = 5;
		Flux<Integer> f = Flux.range(0, limit);
		AtomicInteger counter = new AtomicInteger();
		StepVerifier.create(f)
				.recordWith(ArrayList::new)
				.thenConsumeWhile(i -> counter.incrementAndGet() < limit - 1)
				.consumeRecordedWith(items -> {
					assertThat(items).allMatch(i -> i < limit - 1);
				})
				.recordWith(ArrayList::new) // you can skip this if you want to continue the above session
				.thenConsumeWhile(i -> true)
				.consumeRecordedWith(items -> {
					assertThat(items).containsExactly(limit - 1);
				})
				.verifyComplete();

Alternatively, you can combine the initial consumer in the overload that takes it:

		int limit = 5;
		Flux<Integer> f = Flux.range(0, limit);
		AtomicInteger counter = new AtomicInteger();
		StepVerifier.create(f)
				.recordWith(ArrayList::new)
				.thenConsumeWhile(i -> counter.incrementAndGet() < limit - 1,
						i -> assertThat(i).isLessThan(limit - 1))
				.recordWith(ArrayList::new)
				.thenConsumeWhile(i -> true)
				.consumeRecordedWith(items -> {
					assertThat(items).containsExactly(limit - 1);
				})
				.verifyComplete();

You can also completely avoid the AtomicInteger by using the index() operator:

		int limit = 5;
		Flux<Integer> f = Flux.range(0, limit);
		StepVerifier.create(f.index())
				.recordWith(ArrayList::new)
				.thenConsumeWhile(i -> i.getT1() < limit - 2, // 0-indexed
						i -> assertThat(i.getT2()).isLessThan(limit - 1))
				.recordWith(ArrayList::new)
				.thenConsumeWhile(i -> true)
				.consumeRecordedWith(items -> {
					assertThat(items).map(Tuple2::getT2).containsExactly(limit - 1);
				})
				.verifyComplete();

chemicL avatar May 06 '25 10:05 chemicL