我们编写了一个小型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可用作端点的模拟服务器?
答案 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”语句测试除请求正文以外的所有参数。即使参数之一不匹配,单元测试也会失败,从而断言所有这些。然后,在单独的验证中声明请求主体,并断言“单声道”不能等同。然后使用步骤验证器验证结果。
然后,我们可以使用线模拟进行集成测试,如其他答案中所述,以查看此类是否正确连接,并使用所需的主体调用端点,等等。