Spring Reactor 中的模拟服务

bre*_*Dev 1 java mockito reactor spring-boot spring-webflux

让我们来看看这个简单的方法:

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

所以基本上我想在 service1.doSomething() 会抛出错误的情况下测试这个方法:

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!?
Run Code Online (Sandbox Code Playgroud)

我的问题是为什么 service2.doSomething() 被执行一次?它不应该被执行,因为 service1.doSomething() 在上面抛出一个错误......

Sim*_*slé 5

service2.doSomething()调用该方法的原因是虽然 aMono可以是懒惰的,但简单地调用运算符却不是。您急切地调用将返回 lazyMono的方法,从而组装了一个处理管道。

如果您内联您的代码,我认为它会变得更加清晰:

    //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"))); 
Run Code Online (Sandbox Code Playgroud)

一些运算符接受SupplierorFunction为这种急切的构造风格提供了一种懒惰的替代方案。一种通用的方法是使用Mono.defer

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

但我认为除非service2隐藏了一个不懒惰的源(例如,aMono改编自 a CompletableFuture否则问题不doSomething在于 test

使用service2模拟,您实际上是在测试运算符链的组装,但如果管道中的该步骤实际执行,则不会。

中可用的一个技巧reactor-test是将Mono.just/包装Mono.errorPublisherProbe. 这可以Mono像你一样用来模拟 a ,但增加了在执行时提供断言的功能Mono:它订阅了吗?被要求了吗?

//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();
Run Code Online (Sandbox Code Playgroud)