如何模拟 Spring WebFlux WebClient?
Posted
技术标签:
【中文标题】如何模拟 Spring WebFlux WebClient?【英文标题】:How to mock Spring WebFlux WebClient? 【发布时间】:2017-12-31 06:10:30 【问题描述】:我们编写了一个小型 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 新手,在为这个小代码 sn-p 编写单元测试时遇到了麻烦。
是否有一种优雅的(反应式)方式来模拟 webClient 本身或启动 webClient 可以用作端点的模拟服务器?
【问题讨论】:
【参考方案1】:我们通过提供一个自定义的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
。
来源:
WebClient javadoc WebClient.Builder javadoc ExchangeFunction javadoc【讨论】:
我更喜欢不要在测试中使用.block()
,因为这有点违背如何在 Reactor 中编写测试的主要方法。改用StepVerifer
- 包装你的异步链并断言它的结果。见这里projectreactor.io/docs/test/release/api/reactor/test/…
非常有效的点@povisenko。有时我只是发现在单元测试一些琐碎的事情时使用 .block() 更具可读性。
正如@homeOfTheWizard 提到的,这应该是公认的答案。
@Renette 是的,同意你的看法。 .block()
确实可以满足 medium.com/swlh/stepverifier-vs-block-in-reactor-ca754b12846b
我应该为 ListwebClient.post().uri(url).body(listOfCustomObjects, CustomObject.class)
这就是我的 webclient 调用的编写方式。如何模拟body
方法?通过以下方法,可以使用 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;
【讨论】:
似乎是一个非常简单的解决方案,没有存根 @IgorsSakels 如何用这种方式验证?【参考方案3】:您可以使用 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);
然后可以按以下方式设计测试(与 origin 相比,我使用 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)) )));
【讨论】:
请注意,在引用其他人撰写的材料时,帖子底部的链接不足以给予信任。了解更多here。 在尝试此代码时,我不断收到以下错误...有人能帮忙吗? ApiCallerTest.java:19:错误:无法访问 ExternalResource private final ApiCaller apiCaller = new ApiCaller(WebClient.create(mockWebServer.url("/").toString())); ^ org.junit.rules.ExternalResource 的类文件未找到 这是一个集成测试而不是单元测试。我们不是在这里嘲笑WebClient
@povisenko 这是您的粗鲁评论。但我足够包容,可以把我的知识告诉你。您正在使用模拟服务器模拟响应。您没有在这里完全测试您的 Web 客户端,而是在断言响应。尝试对您的代码 sn-p 运行突变测试/坑测试,它会撕掉这个测试套件。这就是我对你的回答投票赞成 ***.com/a/54254718/2852528 的原因。并对抄袭说不。正如鲍姆在这篇文章的第一条评论中所说,你需要努力阅读这个***.com/help/referencing。祝你好运!
@AkhilGhatiki 我已经更清楚地引用了,同意这是相关评论。我认为您不介意我在集成测试方面的一些清晰性来扩展您的敏锐度。集成测试的常用方法假设将模拟量减少到零。当有很多交叉依赖时,集成测试可以使用模拟,但显然这根本不是我们的情况。我可以建议您查看此 SE 主题至 softwareengineering.stackexchange.com/q/347323/307798 并查看 M. Fowler martinfowler.com/bliki/IntegrationTest.html 的博客文章。干杯!【参考方案4】:
我使用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);
【讨论】:
【参考方案5】:Wire mocks 适用于集成测试,而我认为单元测试不需要它。在进行单元测试时,我只想知道是否使用所需的参数调用了我的 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 方法只接受一个字符串,为简化示例而假定为 Json,在 URI 上执行发布请求并返回假定为字符串的输出响应正文。
该方法可以使用 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' 语句会测试除请求正文之外的所有参数。即使其中一个参数不匹配,单元测试也会失败,从而断言所有这些。然后,请求主体在单独的验证和断言中被断言,因为“单声道”不能等同。然后使用步骤验证器验证结果。
然后,我们可以使用线模拟进行集成测试,如其他答案中所述,以查看此类是否正确连接,并使用所需的主体调用端点等。
【讨论】:
回购代码在哪里?我正在测试它,但它不起作用……它看起来非常好,但不起作用,至少对我来说是这样。【参考方案6】:我已经尝试了这里已经给出的答案中的所有解决方案。 你的问题的答案是: 这取决于您是要进行单元测试还是集成测试。
出于单元测试目的,模拟 WebClient 本身过于冗长并且需要太多代码。模拟 ExchangeFunction 更简单更容易。 为此,接受的答案必须是 @Renette 的解决方案。
对于集成测试,最好的方法是使用 OkHttp MockWebServer。 其使用简单灵活。使用服务器可以让您处理一些错误情况,否则您需要在单元测试用例中手动处理。
【讨论】:
【参考方案7】:我想使用 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());
【讨论】:
【参考方案8】:使用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());
以下是映射文件的示例。 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"
【讨论】:
以上是关于如何模拟 Spring WebFlux WebClient?的主要内容,如果未能解决你的问题,请参考以下文章
spring5 webflux,如何返回自定义json数据?
如何在 Spring boot 2 + Webflux + Thymeleaf 中配置 i18n?