При юнит-тестировании часто требуется проверить методы, которые используют цепочки вызовов, например, в WebClient из Spring WebFlux.
⚠️ Если задача позволяет, лучше воспользоваться MockWebServer или WireMock для гибкого тестирования HTTP-клиентов. Здесь WebClient выбран лишь из-за своей наглядности и популярности.
Представим клиент, запрашивающий случайную цитату у удалённого сервиса:
public class InspirationalQuotesClient {
private final WebClient webClient;
public InspirationalQuotesClient(WebClient webClient) {
this.webClient = webClient;
}
public String fetchRandomQuote() {
try {
return this.webClient
.get()
.uri("/api/quotes")
.retrieve()
.bodyToMono(String.class)
.block();
} catch (WebClientException e) {
return "Every time a mock returns, a mock a fairy dies.";
}
}
}
Для тестирования такого клиента стандартным способом нужно замокировать каждый вызов в цепочке, чтобы избежать
NullPointerException. Это может сделать тест слишком громоздким:
@ExtendWith(MockitoExtension.class)
class InspirationalQuotesClientTest {
@Mock private WebClient webClient;
@InjectMocks private InspirationalQuotesClient cut;
@Test
void shouldReturnInMockingHell() {
WebClient.RequestHeadersUriSpec requestHeadersUriSpec = Mockito.mock(WebClient.RequestHeadersUriSpec.class);
WebClient.ResponseSpec responseSpec = Mockito.mock(WebClient.ResponseSpec.class);
when(webClient.get()).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.uri("/api/quotes")).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.retrieve()).thenReturn(responseSpec);
when(responseSpec.bodyToMono(String.class)).thenReturn(Mono.just("We've escaped hell"));
String result = cut.fetchRandomQuote();
assertEquals("We've escaped hell", result);
}
}
Решение? Deep Stubs в Mockito. С ними можно упростить тест, описав всю цепочку вызовов сразу в конструкции
when. Это значительно уменьшит количество кода:
@ExtendWith(MockitoExtension.class)
class DeepStubClientTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private WebClient webClient;
@InjectMocks private InspirationalQuotesClient cut;
@Test
void shouldReturnQuoteFromRemoteSystem() {
Mockito.when(webClient.get().uri("/api/quotes").retrieve().bodyToMono(String.class))
.thenReturn(Mono.just("Less setup hell - but not better"));
String result = cut.fetchRandomQuote();
assertEquals("Less setup hell - but not better", result);
}
}
Используя параметр
answer = Answers.RETURNS_DEEP_STUBS, мы возвращаем заглушку для каждого метода в цепочке. Но помните, что этот подход увеличивает зависимость теста от конкретной структуры вызовов, и любые изменения в цепочке потребуют изменений в тесте.P.S. А как вы мокируете вызовы внешних API?
Ставьте 🔥 если считаете функциональность полезной.