简体   繁体   中英

Mock services in Spring Reactor

Let's take a look over this simple method:

public Mono<SuccessResponse> doSomething(){
        return service1.doSomething()
            .then(service2.doSomething2())
            .thenReturn(new SuccessResponse("Awesome")));
}

So basically I want to test this method for a scenario in which, service1.doSomething() will throw an error:

when(service1.doSomething()).thenReturn(Mono.error(new IllegalStateException("Something bad happened")));
when(service2.doSomething()).thenReturn(Mono.just(new SomeResponse()))

assertThatThrownBy(() -> testedService.doSomething().block())
            .isExactlyInstanceOf(IllegalStateException.class);

verify(service2, never()).doSomething(); //Why this is executed!?

My question is why service2.doSomething() is executed once? it shouldn't be executed since service1.doSomething() throw an error above...

The reason why the service2.doSomething() method is invoked is that while a Mono can be lazy, plainly calling an operator isn't. You are eagerly calling the methods that will return lazy Mono s, thus assembling a processing pipeline.

If you inline your code, it becomes a bit clearer I think:

    //exception is CREATED immediately, but USED lazily
return Mono.error(new IllegalStateException())
    //mono is CREATED immediately. The data it will emit is also CREATED immediately. But it all triggers LAZILY.
    .then(Mono.just(new SomeResponse()))
    //note that then* operators completely ignore previous step's result (unless it is an error)
    .thenReturn(new SuccessResponse("Awesome"))); 

Some operators accept Supplier or Function which provides a lazy alternative to this eager construction style. One universal way of doing that is to use Mono.defer :

public Mono<SuccessResponse> doSomething(){
        return service1.doSomething()
            .then(Mono.defer(service2::doSomething2))
            .thenReturn(new SuccessResponse("Awesome")));
}

But I'd argue that , unless service2 hides a source that is NOT lazy (eg. a Mono adapted from a CompletableFuture ) , the problem is not the doSomething but the test .

With the service2 mock, you are essentially testing the assembly of the chain of operators, but not if that step in the pipeline is actually executed.

One trick available in reactor-test is to wrap the Mono.just / Mono.error in a PublisherProbe . This can be used to mock a Mono like you did, but with the added feature of providing assertions on the execution of the Mono : was it subscribed to? was it requested?

//this is ultimately tested by the assertThrownBy, let's keep it that way:
when(service1.doSomething()).thenReturn(Mono.error(new IllegalStateException("Something bad happened")));

//this will be used to ensure the `service2` Mono is never actually used: 
PublisherProbe<SomeResponse> service2Probe = PublisherProbe.of(Mono.just(new SomeResponse()));
//we still need the mock to return a Mono version of our probe
when(service2.doSomething()).thenReturn(service2Probe.mono());

assertThatThrownBy(() -> testedService.doSomething().block())
            .isExactlyInstanceOf(IllegalStateException.class);

//service2 might have returned a lazy Mono, but it was never actually used:
probe.assertWasNotSubscribed();

thenReturn is not for throwing error ! You need to use thenThrow() and also you don't need to write mock method for service2 , just do verify which have called

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM