如何在单元测试中模拟Spring WebClient

时间:2017-07-25 10:53:46

标签: spring rest unit-testing mocking reactive-programming

我们编写了一个小型Spring Boot REST应用程序,它在另一个REST端点上执行REST请求。

@RequestMapping("/api/v1")
@SpringBootApplication
@RestController
@Slf4j
public class Application
{
    @Autowired
    private WebClient webClient;

    @RequestMapping(value = "/zyx", method = POST)
    @ResponseBody
    XyzApiResponse zyx(@RequestBody XyzApiRequest request, @RequestHeader HttpHeaders headers)
    {
        webClient.post()
            .uri("/api/v1/someapi")
            .accept(MediaType.APPLICATION_JSON)
            .contentType(MediaType.APPLICATION_JSON)
            .body(BodyInserters.fromObject(request.getData()))
            .exchange()
            .subscribeOn(Schedulers.elastic())
            .flatMap(response ->
                    response.bodyToMono(XyzServiceResponse.class).map(r ->
                    {
                        if (r != null)
                        {
                            r.setStatus(response.statusCode().value());
                        }

                        if (!response.statusCode().is2xxSuccessful())
                        {
                            throw new ProcessResponseException(
                                    "Bad status response code " + response.statusCode() + "!");
                        }

                        return r;
                    }))
            .subscribe(body ->
            {
                // Do various things
            }, throwable ->
            {
                // This section handles request errors
            });

        return XyzApiResponse.OK;
    }
}

我们是Spring的新手,无法为这个小代码片段编写单元测试。

是否有优雅(反应)方式来模拟webClient本身或启动webClient可用作端点的模拟服务器?

7 个答案:

答案 0 :(得分:3)

我们通过提供自定义ExchangeFunction来完成此任务,该自定义WebClientBuiler仅将我们想要的响应返回给 webClient = WebClient.builder() .exchangeFunction(clientRequest -> Mono.just(ClientResponse.create(HttpStatus.OK) .header("content-type", "application/json") .body("{ \"key\" : \"value\"}") .build()) ).build(); myHttpService = new MyHttpService(webClient); Map<String, String> result = myHttpService.callService().block(); // Do assertions here

@Mock
private ExchangeFunction exchangeFunction;

@BeforeEach
void init() {
    WebClient webClient = WebClient.builder()
            .exchangeFunction(exchangeFunction)
            .build();

    myHttpService = new MyHttpService(webClient);
}

@Test
void callService() {
    when(exchangeFunction.exchange(any(ClientRequest.class))).thenReturn(buildMockResponse());
    Map<String, String> result = myHttpService.callService().block();

    verify(exchangeFunction).exchange(any());

    // Do assertions here
}

如果我们想使用Mokcito来验证是否进行了调用或在类中的多个单元测试之间重用WebClient,我们还可以模拟交换功能:

when

注意:如果在Mono.when调用中获得与发布者相关的空指针异常,则您的IDE可能已导入Mockito.when而不是<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>JQuery Drag Multiple</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>jQuery UI Draggable - Default functionality</title> <link rel="stylesheet" href="jquery-ui.css"> <style> .container { width: 500px; height: 500px; border: 1px solid silver; position: relative; top: 60px; left: 60px; } .box { width: 60px; height: 60px; border: 1px solid black; position: absolute; } #draggable1 { top: 0px; } #draggable2 { top: 60px; } #draggable3 { top: 120px; } #draggable4 { top: 180px; } </style> <script src="jquery.min.js"></script> <script src="jquery-ui.js"></script> <script src="drag-multiple.js"></script> <script> /*$(function () { $(".box").draggable({multiple: true}); });*/ $('.box').each(function(index) { var options = { multiple: true, }; $(this).draggable(options); }); </script> </head> <body> <div class="container"> <div id="draggable1" class="box ui-selected"> <p>Drag 1</p> </div> <div id="draggable2" class="box"> <p>Drag 2</p> </div> <div id="draggable3" class="box ui-selected"> <p>Drag 3</p> </div> <div id="draggable4" class="box ui-selected"> <p>Drag 4</p> </div> </div> </body> </html>

来源:

答案 1 :(得分:3)

我已经尝试过这里已经给出的答案中的所有解决方案。 您的问题的答案是: 这取决于您要进行单元测试还是集成测试。

出于单元测试的目的,模拟WebClient本身太冗长,并且需要太多代码。模拟ExchangeFunction更容易。 为此,可接受的答案必须是@Renette的解决方案。

对于集成测试,最好的方法是使用OkHttp MockWebServer。 它使用灵活简单。使用服务器可以处理一些错误情况,否则在单元测试用例中需要手动处理。

答案 2 :(得分:3)

我使用WireMock进行集成测试。我认为它比OkHttp MockeWebServer更好,并且支持更多功能。这是简单的示例:

public class WireMockTest {

  WireMockServer wireMockServer;
  WebClient webClient;

  @BeforeEach
  void setUp() throws Exception {
    wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
    wireMockServer.start();
    webClient = WebClient.builder().baseUrl(wireMockServer.baseUrl()).build();
  }

  @Test
  void testWireMock() {
    wireMockServer.stubFor(get("/test")
        .willReturn(ok("hello")));

    String body = webClient.get()
        .uri("/test")
        .retrieve()
        .bodyToMono(String.class)
        .block();
    assertEquals("hello", body);
  }

  @AfterEach
  void tearDown() throws Exception {
    wireMockServer.stop();
  }

}

如果您真的想模拟它,我建议JMockit。不必多次调用when,并且可以像在经过测试的代码中一样使用相同的调用。

@Test
void testJMockit(@Injectable WebClient webClient) {
  new Expectations() {{
      webClient.get()
          .uri("/test")
          .retrieve()
          .bodyToMono(String.class);
      result = Mono.just("hello");
  }};

  String body = webClient.get()
      .uri(anyString)
      .retrieve()
      .bodyToMono(String.class)
      .block();
  assertEquals("hello", body);
}

答案 3 :(得分:2)

我认为内置弹簧支持仍在进行中 - https://jira.spring.io/browse/SPR-15286

我真的很喜欢wiremock(整合 - )测试这样的场景。特别是因为您使用此测试整个序列化和反序列化。使用wiremock,您可以启动一个服务器,使用预定义的存根来处理您的请求。

答案 4 :(得分:1)

通过以下方法,可以使用Mockito模拟WebClient进行如下调用:

webClient
.get()
.uri(url)
.header(headerName, headerValue)
.retrieve()
.bodyToMono(String.class);

webClient
.get()
.uri(url)
.headers(hs -> hs.addAll(headers));
.retrieve()
.bodyToMono(String.class);

模拟方法:

private static WebClient getWebClientMock(final String resp) {
    final var mock = Mockito.mock(WebClient.class);
    final var uriSpecMock = Mockito.mock(WebClient.RequestHeadersUriSpec.class);
    final var headersSpecMock = Mockito.mock(WebClient.RequestHeadersSpec.class);
    final var responseSpecMock = Mockito.mock(WebClient.ResponseSpec.class);

    when(mock.get()).thenReturn(uriSpecMock);
    when(uriSpecMock.uri(ArgumentMatchers.<String>notNull())).thenReturn(headersSpecMock);
    when(headersSpecMock.header(notNull(), notNull())).thenReturn(headersSpecMock);
    when(headersSpecMock.headers(notNull())).thenReturn(headersSpecMock);
    when(headersSpecMock.retrieve()).thenReturn(responseSpecMock);
    when(responseSpecMock.bodyToMono(ArgumentMatchers.<Class<String>>notNull()))
            .thenReturn(Mono.just(resp));

    return mock;
}

答案 5 :(得分:0)

您可以由OkHttp团队使用MockWebServer。基本上,Spring团队也将其用于测试(至少他们怎么说here)。这是一个使用此blog post中的代码的示例:

让我们考虑我们提供以下服务

class ApiCaller {

    private WebClient webClient;

    ApiCaller(WebClient webClient) {
        this.webClient = webClient;
    }

    Mono<SimpleResponseDto> callApi() {
        return webClient.put()
                .uri("/api/resource")
                .contentType(MediaType.APPLICATION_JSON)
                .header("Authorization", "customAuth")
                .syncBody(new SimpleRequestDto())
                .retrieve()
                .bodyToMono(SimpleResponseDto.class);
    }
}

然后可以以一种雄辩的方式设计测试:

class ApiCallerTest {

    private final MockWebServer mockWebServer = new MockWebServer();
    private final ApiCaller apiCaller = new ApiCaller(WebClient.create(mockWebServer.url("/").toString()));

    @AfterEach
    void tearDown() throws IOException {
        mockWebServer.shutdown();
    }

    @Test
    void call() throws InterruptedException {
        mockWebServer.enqueue(
                new MockResponse()
                        .setResponseCode(200)
                        .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                        .setBody("{\"y\": \"value for y\", \"z\": 789}")
        );
        SimpleResponseDto response = apiCaller.callApi().block();
        assertThat(response, is(not(nullValue())));
        assertThat(response.getY(), is("value for y"));
        assertThat(response.getZ(), is(789));

        RecordedRequest recordedRequest = mockWebServer.takeRequest();
        //use method provided by MockWebServer to assert the request header
        recordedRequest.getHeader("Authorization").equals("customAuth");
        DocumentContext context = JsonPath.parse(recordedRequest.getBody().inputStream());
        //use JsonPath library to assert the request body
        assertThat(context, isJson(allOf(
                withJsonPath("$.a", is("value1")),
                withJsonPath("$.b", is(123))
        )));
    }
}

答案 6 :(得分:0)

Wire模拟适用于集成测试,而我认为单元测试不需要。在进行单元测试时,我只是想知道是否使用所需的参数调用了WebClient。为此,您需要模拟WebClient实例。或者,您也可以注入WebClientBuilder。

让我们考虑简化的方法,该方法执行如下所示的发布请求。

@Service
@Getter
@Setter
public class RestAdapter {

    public static final String BASE_URI = "http://some/uri";
    public static final String SUB_URI = "some/endpoint";

    @Autowired
    private WebClient.Builder webClientBuilder;

    private WebClient webClient;

    @PostConstruct
    protected void initialize() {
        webClient = webClientBuilder.baseUrl(BASE_URI).build();
    }

    public Mono<String> createSomething(String jsonDetails) {

        return webClient.post()
                .uri(SUB_URI)
                .accept(MediaType.APPLICATION_JSON)
                .body(Mono.just(jsonDetails), String.class)
                .retrieve()
                .bodyToMono(String.class);
    }
}

createSomething方法仅接受一个String(为示例简单起见,假定为Json),对URI进行发布请求,并返回假定为String的输出响应正文。

可以使用StepVerifier对方法进行如下单元测试。

public class RestAdapterTest {
    private static final String JSON_INPUT = "{\"name\": \"Test name\"}";
    private static final String TEST_ID = "Test Id";

    private WebClient.Builder webClientBuilder = mock(WebClient.Builder.class);
    private WebClient webClient = mock(WebClient.class);

    private RestAdapter adapter = new RestAdapter();
    private WebClient.RequestBodyUriSpec requestBodyUriSpec = mock(WebClient.RequestBodyUriSpec.class);
    private WebClient.RequestBodySpec requestBodySpec = mock(WebClient.RequestBodySpec.class);
    private WebClient.RequestHeadersSpec requestHeadersSpec = mock(WebClient.RequestHeadersSpec.class);
    private WebClient.ResponseSpec responseSpec = mock(WebClient.ResponseSpec.class);

    @BeforeEach
    void setup() {
        adapter.setWebClientBuilder(webClientBuilder);
        when(webClientBuilder.baseUrl(anyString())).thenReturn(webClientBuilder);
        when(webClientBuilder.build()).thenReturn(webClient);
        adapter.initialize();
    }

    @Test
    @SuppressWarnings("unchecked")
    void createSomething_withSuccessfulDownstreamResponse_shouldReturnCreatedObjectId() {
        when(webClient.post()).thenReturn(requestBodyUriSpec);
        when(requestBodyUriSpec.uri(RestAdapter.SUB_URI))
                .thenReturn(requestBodySpec);
        when(requestBodySpec.accept(MediaType.APPLICATION_JSON)).thenReturn(requestBodySpec);
        when(requestBodySpec.body(any(Mono.class), eq(String.class)))
                .thenReturn(requestHeadersSpec);
        when(requestHeadersSpec.retrieve()).thenReturn(responseSpec);
        when(responseSpec.bodyToMono(String.class)).thenReturn(Mono.just(TEST_ID));


        ArgumentCaptor<Mono<String>> captor
                = ArgumentCaptor.forClass(Mono.class);

        Mono<String> result = adapter.createSomething(JSON_INPUT);

        verify(requestBodySpec).body(captor.capture(), eq(String.class));
        Mono<String> testBody = captor.getValue();
        assertThat(testBody.block(), equalTo(JSON_INPUT));
        StepVerifier
                .create(result)
                .expectNext(TEST_ID)
                .verifyComplete();
    }
}

请注意,“ when”语句测试除请求正文以外的所有参数。即使参数之一不匹配,单元测试也会失败,从而断言所有这些。然后,在单独的验证中声明请求主体,并断言“单声道”不能等同。然后使用步骤验证器验证结果。

然后,我们可以使用线模拟进行集成测试,如其他答案中所述,以查看此类是否正确连接,并使用所需的主体调用端点,等等。