如何模拟Spring WebFlux WebClient?

vhmi4jdf  于 2022-12-21  发布在  Spring
关注(0)|答案(9)|浏览(203)

我们编写了一个小型的 Boot 应用程序,它在另一个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的新手,在为这个小代码段编写单元测试时遇到了麻烦。
有没有一种优雅的(React式的)方法来模拟webClient本身,或者启动一个webClient可以用作端点的模拟服务器?

ao218c7q

ao218c7q1#

我们通过提供一个定制的ExchangeFunction来实现这一点,该定制的ExchangeFunction只向WebClientBuilder返回我们想要的响应:

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

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

@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
}

注意:如果在when调用中出现与发布者相关的空指针异常,则IDE可能导入了Mono.when而不是Mockito.when

g6baxovj

g6baxovj2#

使用下面的方法,可以使用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;
}
4ioopgfo

4ioopgfo3#

OkHttp团队可以使用MockWebServer,Spring团队基本上也在测试中使用它(至少他们是这样说here的),下面是一个参考源代码的示例:
根据Tim's 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);
    }
}

然后,可以按照以下方式设计测试(与原始测试相比,我更改了应如何使用StepVerifier在Reactor中测试异步链的方式):

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}")
      );
      
      //Asserting response
      StepVerifier.create(apiCaller.callApi())
                  .assertNext(res -> {
                        assertNotNull(res);
                        assertEquals("value for y", res.getY());
                        assertEquals("789", res.getZ());
                  })
                  .verifyComplete();
 
     //Asserting request
     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))
     )));
  }
}
m4pnthwp

m4pnthwp4#

我使用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);
}
3vpjnl9f

3vpjnl9f5#

Wire mocks适用于集成测试,而我认为单元测试不需要它。在进行单元测试时,我只想知道我的WebClient是否被调用了所需的参数。为此,您需要一个WebClient示例的mock。或者,您可以注入一个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上执行一个post请求,并返回输出响应主体(假设为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”语句测试除请求主体之外的所有参数。即使其中一个参数不匹配,单元测试也会失败,从而Assert所有这些参数。然后,在单独的验证中Assert请求主体,并Assert“Mono”无法相等。然后使用步骤验证器验证结果。
然后,我们可以使用wire mock进行集成测试,如其他答案中所述,以查看该类是否正确连接,以及是否使用所需主体调用端点等。

tcbh2hod

tcbh2hod6#

我已经尝试了所有的解决方案在这里已经给出的答案。你的问题的答案是:这取决于你想做单元测试还是集成测试。
对于单元测试的目的,模拟WebClient本身太冗长,需要太多的代码。模拟Exchange函数更简单,更容易。对于这一点,可接受的答案必须是@Renette的解决方案。
对于集成测试来说,最好使用OkHttp MockWebServer。它使用起来很简单,也很灵活。使用服务器可以让你处理一些错误情况,否则你需要在单元测试中手动处理。

bqf10yzr

bqf10yzr7#

使用spring-cloud-starter-contract-stub-runner,您可以使用Wiremock模拟API响应。Here,您可以找到我在medium上描述的一个工作示例。AutoConfigureMockMvc注解在测试之前启动一个Wiremock服务器,公开classpath:/mappings位置中的所有内容(可能是磁盘上的src/test/resources/mappings)。

@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureWireMock(port = 0)
class BalanceServiceTest {
    private static final Logger log = LoggerFactory.getLogger(BalanceServiceTest.class);
    @Autowired
    private BalanceService service;

    @Test
    public void test() throws Exception {

        assertNotNull(service.getBalance("123")
                .get());
    }
}

下面是一个Map文件的例子。balance.json文件包含了你所需要的任何json内容。你也可以在静态配置文件中或编程中模拟响应延迟或失败。更多关于website的信息。

{
  "request": {
    "method": "GET",
    "url": "/v2/accounts/123/balance"
  },
  "response": {
    "status": 200,
    "delayDistribution": {
      "type": "lognormal",
      "median": 1000,
      "sigma": 0.4
    },
    "headers": {
      "Content-Type": "application/json",
      "Cache-Control": "no-cache"
    },
    "bodyFileName": "balance.json"
  }
}
pkwftd7m

pkwftd7m8#

我想使用webclient进行单元测试,但是mockito太复杂了,所以我创建了一个library,它可以用来在单元测试中构建mock webclient,它也可以在发送响应之前验证url,方法,头和请求主体。

FakeWebClientBuilder fakeWebClientBuilder = FakeWebClientBuilder.useDefaultWebClientBuilder();
    
            FakeRequestResponse fakeRequestResponse = new FakeRequestResponseBuilder()
            .withRequestUrl("https://google.com/foo")
            .withRequestMethod(HttpMethod.POST)
            .withRequestBody(BodyInserters.fromFormData("foo", "bar"))
            .replyWithResponse("test")
            .replyWithResponseStatusCode(200)
            .build();
    
    
    
            WebClient client =
            FakeWebClientBuilder.useDefaultWebClientBuilder()
            .baseUrl("https://google.com")
            .addRequestResponse(fakeRequestResponse)
            .build();
    
            // Our webclient will return `test` when called. 
           // This assertion would check if all our enqueued responses are dequeued by the class or method we intend to test.
           Assertions.assertTrue(fakeWebClientBuilder.assertAllResponsesDispatched());
mitkmikd

mitkmikd9#

我强烈推荐使用Okhttp MockWebServer而不是mocking。原因是MockWebServer是一种干净得多的方法。
下面是可用于单元测试WebClient的代码模板。

class Test {

  private ClassUnderTest classUnderTest;
  public static MockWebServer mockWebServer;

  @BeforeAll
  static void setUp() throws IOException {
    mockWebServer = new MockWebServer();
    mockWebServer.start();
  }

  @BeforeEach
  void initialize() {
    var httpUrl = mockWebServer.url("/xyz");
    var webClient = WebClient.create(httpUrl.toString());
    classUnderTest = new ClassUnderTest(webClient);
  }

  @Test
  void testMehod() {
    var mockResp = new MockResponse();
    mockResp.setResponseCode(200);
    mockResp.addHeader("Content-Type", "application/json");
    mockResp.setBody(
        "{\"prop\":\"some value\"}");
    mockWebServer.enqueue(mockResp); 
    // This enqueued response will be returned when webclient is invoked
    ...
    ...
    classUnderTest.methodThatInvkesWebClient();
    ...
    ...
  }

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

要特别注意initialize方法,这是这里唯一需要技巧的地方。
路径/xyz不是基本url,而是您的资源路径。您不需要将基本url告知MockWebServer。原因是,MockWebServer将使用某个随机端口在本地主机上启动服务器。如果您提供自己的基本url,则单元测试将失败。
mockWebServer.url("/xyz")
这将为您提供基本url,即MockWebServer正在侦听的主机和端口,以及资源路径,例如localhost:8999/xyz。您需要使用此url创建WebClient。
WebClient.create(httpUrl.toString())
这将创建一个WebClient,用于为您的单元测试调用MockWebServer。

相关问题