Как издеваться над Spring WebFlux WebClient?
Мы написали небольшое приложение 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 и не можем написать Unit Test для этого небольшого фрагмента кода.
Есть ли элегантный (реактивный) способ издеваться над самим webClient или запускать макетный сервер, который webClient может использовать в качестве конечной точки?
Ответы
Ответ 1
Я думаю, что встроенная поддержка spring для этого все еще продолжается - https://jira.spring.io/browse/SPR-15286
Мне действительно нравится wiremock, чтобы (интеграция) проверять такие сценарии. Тем более, что вы проверяете всю сериализацию и десериализацию с этим. С помощью wiremock вы запускаете сервер, который обслуживает ваши запросы, используя предопределенные заглушки.
Ответ 2
С помощью следующего метода можно было смоделировать WebClient с помощью Mockito для таких вызовов:
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;
}
Ответ 3
Вы можете использовать MockWebServer командой OkHttp. По сути, команда Spring использует его и для своих тестов (по крайней мере, как они сказали здесь). Вот пример использования кода из этого сообщения блога:
Давайте рассмотрим, что у нас есть следующий сервис
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))
)));
}
}
Ответ 4
Мне удалось смоделировать метод get, однако сообщение, что я получаю исключение нулевого указателя при запуске получения. Любая идея?
Ответ 5
Проводные макеты подходят для интеграционных тестов, хотя я считаю, что они не нужны для модульных тестов. При выполнении модульных тестов мне будет просто интересно узнать, был ли вызван мой 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 для простоты примера, выполняет запрос post на 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 проверяют все параметры, кроме тела запроса. Даже если один из параметров не совпадает, модульное тестирование завершается неудачно, подтверждая все это. Затем тело запроса утверждается в отдельной проверке и утверждении, поскольку "Mono" не может быть приравнено. Затем результат проверяется с помощью пошагового верификатора.
И затем, мы можем выполнить интеграционный тест с проводным макетом, как упомянуто в других ответах, чтобы увидеть, правильно ли подключается этот класс, и вызывает ли конечную точку нужное тело и т.д.