Spring Reactor 中的模拟服务

Mock services in Spring Reactor

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

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

所以基本上我想针对 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!?

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

thenReturn 不是为了抛出错误!您需要使用 thenThrow() 并且您不需要为 service2 编写模拟方法,只需验证哪些调用了

调用 service2.doSomething() 方法的原因是,虽然 Mono 可以是惰性的,但显然调用运算符不是。您正在急切地调用将 return 惰性 Mono 的方法,从而组装一个处理管道。

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

    //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"))); 

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

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

但我认为除非service2隐藏了一个不懒惰的来源(例如Mono改编来自CompletableFuture)问题不是doSomething而是测试.

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

reactor-test 中可用的一个技巧是将 Mono.just/Mono.error 包装在 PublisherProbe 中。这可以像您一样用于模拟 Mono,但具有在 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();