如何模拟 Spring WebFlux WebClient?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/45301220/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to mock Spring WebFlux WebClient?
提问by Roman
We wrote a small Spring Boot REST application, which performs a REST request on another REST endpoint.
我们编写了一个小型 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;
}
}
We are new to Spring and are having trouble writing a Unit Test for this small code snippet.
我们是 Spring 的新手,在为这个小代码片段编写单元测试时遇到了麻烦。
Is there an elegant (reactive) way to mock the webClient itself or to start a mock server that the webClient can use as an endpoint?
是否有一种优雅的(反应式)方式来模拟 webClient 本身或启动 webClient 可以用作端点的模拟服务器?
回答by Renette
We accomplished this by providing a custom ExchangeFunctionthat simply returns the response we want to the WebClientBuiler:
我们通过提供一个自定义来实现这一点,该自定义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
If we want to use Mokcito to verify if the call was made or reuse the WebClient accross multiple unit tests in the class, we could also mock the exchange function:
如果我们想使用 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
}
Note: If you get null pointer exceptions related to publishers on the whencall, your IDE might have imported Mono.wheninstead of Mockito.when.
注意:如果您在when调用时遇到与发布者相关的空指针异常,则您的 IDE 可能导入Mono.when了Mockito.when.
Sources:
资料来源:
回答by povisenko
You can use MockWebServerby the OkHttp team. Basically, the Spring team uses it for their tests too (at least how they said here). Here is an example with reference to a source:
您可以使用MockWebServer由OkHttp队。基本上,Spring 团队也将它用于他们的测试(至少他们在这里是这么说的)。这是一个参考来源的示例:
According to Tim's blog postlet's consider that we have the following service:
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); } }
根据Tim 的博客文章,让我们考虑一下我们有以下服务:
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); } }
then the test could be designed in such an eloquent way:
那么测试就可以用这样一种雄辩的方式来设计:
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)) ))); } }
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)) ))); } }
回答by Igors Sakels
With the following method it was possible to mock the WebClient with Mockito for calls like this:
使用以下方法,可以使用 Mockito 模拟 WebClient 以进行这样的调用:
webClient
.get()
.uri(url)
.header(headerName, headerValue)
.retrieve()
.bodyToMono(String.class);
or
或者
webClient
.get()
.uri(url)
.headers(hs -> hs.addAll(headers));
.retrieve()
.bodyToMono(String.class);
Mock method:
模拟方法:
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;
}
回答by Sankaran Srinivasan
Wire mocks is suitable for integration tests, while I believe it's not needed for unit tests. While doing unit tests, I will just be interested to know if my WebClient was called with the desired parameters. For that you need a mock of the WebClient instance. Or you could inject a WebClientBuilder instead.
Wire mocks 适用于集成测试,而我认为单元测试不需要它。在进行单元测试时,我只想知道是否使用所需的参数调用了我的 WebClient。为此,您需要模拟 WebClient 实例。或者您可以改为注入 WebClientBuilder。
Let's consider the simplified method which does a post request like below.
让我们考虑执行如下发布请求的简化方法。
@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);
}
}
The method createSomething just accepts a String, assumed as Json for simplicity of the example, does a post request on a URI and returns the output response body which is assumed as a String.
方法 createSomething 只接受一个字符串,为简单起见假定为 Json,在 URI 上执行 post 请求并返回输出响应正文,假定为字符串。
The method can be unit tested as below, with StepVerifier.
该方法可以使用 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();
}
}
Note that the 'when' statements test all the parameters except the request Body. Even if one of the parameters mismatches, the unit test fails, thereby asserting all these. Then, the request body is asserted in a separate verify and assert as the 'Mono' cannot be equated. The result is then verified using step verifier.
请注意,“when”语句测试除请求正文之外的所有参数。即使其中一个参数不匹配,单元测试也会失败,从而断言所有这些。然后,请求正文在单独的验证和断言中被断言,因为“Mono”不能被等同。然后使用步骤验证器验证结果。
And then, we can do an integration test with wire mock, as mentioned in the other answers, to see if this class wires properly, and calls the endpoint with the desired body, etc.
然后,我们可以使用 wire mock 进行集成测试,如其他答案中所述,以查看此类是否正确连接,并使用所需的主体调用端点等。
回答by homeOfTheWizard
I have tried all the solutions in the already given answers here. The answer to your question is: It depends if you want to do Unit testing or Integration testing.
我已经尝试了这里已经给出的答案中的所有解决方案。您的问题的答案是:这取决于您是要进行单元测试还是集成测试。
For unit testing purpose, mocking the WebClient itself is too verbose and require too much code. Mocking ExchangeFunction is simpler and easier. For this, the accepted answer must be @Renette 's solution.
出于单元测试的目的,模拟 WebClient 本身过于冗长并且需要太多代码。模拟 ExchangeFunction 更简单更容易。为此,接受的答案必须是 @Renette 的解决方案。
For integration testing the best is to use OkHttp MockWebServer. Its simple to use an flexible. Using a server allows you to handle some error cases you otherwise need to handle manually in a Unit testing case.
对于集成测试,最好使用 OkHttp MockWebServer。它的使用简单灵活。使用服务器允许您处理一些错误情况,否则您需要在单元测试情况下手动处理。
回答by SalHyman
I use WireMockfor integration testing. I think it is much better and supports more functions than OkHttp MockeWebServer. Here is simple example:
我使用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();
}
}
If you really want to mock it I recommend JMockit. There isn't necessary call whenmany times and you can use the same call like it is in your tested code.
如果你真的想模拟它,我推荐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);
}

