Spring Webflux Mockito - 模拟 Webclient 调用的响应

Spring Webflux Mockito - mock the response of a Webclient call

关于如何在单元测试阶段“强制”或“模拟”Webclient http 调用的响应的小问题。

我有一个非常简单的方法:

public String question() {
  String result = getWebClient().mutate().baseUrl(someUrlVariable).build().post().uri("/test").retrieve().bodyToMono(String).block();
   if (result == null) {
     doSomething1();
   }
 if (result.equals("")) {
     doSomething2();
   }
 if (result.equals("foo")) {
     doSomething3();
   }

如您所见,此方法的复杂部分是 Webclient 调用。它有(在本例中)7 个 .method(),如 .mutate()、.post()。等...

在我的用例中,我根本没有兴趣测试这个 Webclient。

我希望 Mockito 的功能在某种程度上相当于:

public String question() {
  // it is just unit test. Mockito, please just return me the string I tell you to return please. Don't even execute this next line if possible, just return me this dummy response
  String result = the-thing-I-tell-mockito-to-return;
   if (result == null) {
     doSomething1();
   }
 if (result.equals("")) {
     doSomething2();
   }
 if (result.equals("foo")) {
     doSomething3();
   }

到目前为止,我尝试了整行的 Mockito doNothing()Mockito.when(getWebclient()... ) 加上 .thenReturn,但没有成功。

请问如何实现?

您必须首先确保 getWebclient() return 是模拟。根据您现有的代码示例,我无法判断这是针对不同的 class 还是私有方法(通过构造函数注入 WebClientWebClient.Builder 可能有意义) .

接下来,您必须使用 Mockito 模拟整个方法链。这几乎包括 copy/pasting 您的整个实施:

when(webClient.mutate()).thenReturn(webClient);
when(webClient.baseUrl(yourUrl)).thenReturn(...);
// etc. 

Mockito 可以 return 深度存根(检查 documentation 并搜索 RETURN_DEEP_STUBS)可以简化这个存根设置。

但是,对于您的 WebClient 测试和模拟 HTTP 响应,更好的 解决方案是 to spawn a local HTTP server。这涉及较少的 Mockito 仪式,还允许测试错误场景(不同的 HTTP 响应、缓慢的响应等),

I would like to avoid those copy/pasting of when()

好吧,你已经设计了你的代码,所以测试它的唯一方法是复制粘贴 when

那你是怎么设计的呢?好吧,您将 API 代码与逻辑混合在一起,这是您不应该做的事情。编写测试时首先要考虑的是“我要测试什么?”,答案通常是业务逻辑

如果我们查看您的代码:

public String question() {
    // This is api code, we dont want to test this, 
    // spring has already tested this for us.
    String result = getWebClient()
                     .mutate()
                     .baseUrl(someUrlVariable)
                     .build()
                     .post()
                     .uri("/test")
                     .retrieve()
                     .bodyToMono(String)
                     .block();

    // This is logic, this is want we want to test
    if (result == null) {
        doSomething1();
    }
    if (result.equals("")) {
         doSomething2();
    }
    if (result.equals("foo")) {
        doSomething3();
    }
}

当我们设计一个应用程序时,我们将它分成几层,通常是一个前端 api (RestController),然后是中间的业务逻辑 (Controllers),最后是调用其他的不同资源 apis(存储库、资源等)

因此,当涉及到您的应用程序时,我会重新设计它,拆分 api 和逻辑:

@Bean
@Qualifier("questionsClient") 
public WebClient webclient(WebClient.Builder webClient) {
    return webClient.baseUrl("https://foobar.com")
                .build();
}

// This class responsibility is to fetch, and do basic validation. Ensure
// That whatever is returned from its functions is a concrete value.
// Here you should handle things like basic validation and null.
@Controller
public class QuestionResource {

    private final WebClient webClient;
    
    public QuestionResource(@Qualifier("questionsClient") WebClient webClient) {
        this.webClient = webClient;
    }


    public String get(String path) {
        return webClient.post()
                     .uri(path)
                     .retrieve()
                     .bodyToMono(String)
                     .block();
    }
}


// In this class we make business decisions on the values we have.
// If we get a "Foo" we do this. If we get a "Bar" we do this.
@Controller
public class QuestionHandler {

    private final QuestionResource questionResource;
    
    public QuestionResource(QuestionResource questionResource) {
        this.questionResource = questionResource;
    }

    public String get() {
        final String result = questionResource.get("/test");
        
        // also i dont see how the response can be null.
        // Null should never be considered a value and should not be let into the logic. 
        // Because imho. its a bomb. Anything that touches null will explode (NullPointerException). 
        // Null should be handled in the layer before.
        if (result == null) {
            return doSomething1();
        }
        if (result.equals("")) {
             return doSomething2();
        }
        if (result.equals("foo")) {
             return doSomething3();
        }
    }
}

那么在你的测试中:

@Test
public void shouldDoSomething() {
    final QuestionResource questionResourceMock = mock(QuestionResource.class);
    when(questionResourceMock.get("/test")).thenReturn("");
    
    final QuestionHandler questionHandler = new QuestionHandler(questionResourceMock);
    final String something = questionHandler.get();
    
    // ...
    // assert etc. etc.
}

此外,我建议您不要改变网络客户端,为每个网络客户端创建一个 api,因为它很快就会变得混乱。

本文没有IDE,因此可能存在编译错误等