«1. Обзор
В настоящее время мы ожидаем вызова REST API в большинстве наших сервисов. Spring предоставляет несколько вариантов создания REST-клиента, рекомендуется использовать WebClient.
В этом кратком руководстве мы рассмотрим, как проводить модульное тестирование сервисов, использующих WebClient для вызова API.
2. Насмешка
У нас есть два основных варианта насмешек в наших тестах:
-
Использовать Mockito для имитации поведения WebClient Использовать WebClient по-настоящему, но имитировать вызываемый им сервис с помощью MockWebServer (okhttp) ~~ ~ 3. Использование Mockito
Mockito — самая распространенная библиотека для создания макетов для Java. Это хорошо для предоставления предопределенных ответов на вызовы методов, но все становится сложнее, когда они имитируют плавные API. Это связано с тем, что в свободном API между вызывающим кодом и макетом передается множество объектов.
Например, пусть у нас есть класс EmployeeService с методом getEmployeeById для получения данных по HTTP с помощью WebClient:
Мы можем использовать Mockito для имитации этого: чтобы предоставить другой фиктивный объект для каждого вызова в цепочке, с четырьмя различными требуемыми вызовами when/thenReturn. Это многословно и громоздко. Это также требует от нас знания деталей реализации того, как именно наш сервис использует WebClient, что делает этот способ тестирования ненадежным.
public class EmployeeService {
public EmployeeService(String baseUrl) {
this.webClient = WebClient.create(baseUrl);
}
public Mono<Employee> getEmployeeById(Integer employeeId) {
return webClient
.get()
.uri("http://localhost:8080/employee/{id}", employeeId)
.retrieve()
.bodyToMono(Employee.class);
}
}
Как мы можем написать лучшие тесты для WebClient?
@ExtendWith(MockitoExtension.class)
public class EmployeeServiceTest {
@Test
void givenEmployeeId_whenGetEmployeeById_thenReturnEmployee() {
Integer employeeId = 100;
Employee mockEmployee = new Employee(100, "Adam", "Sandler",
32, Role.LEAD_ENGINEER);
when(webClientMock.get())
.thenReturn(requestHeadersUriSpecMock);
when(requestHeadersUriMock.uri("/employee/{id}", employeeId))
.thenReturn(requestHeadersSpecMock);
when(requestHeadersMock.retrieve())
.thenReturn(responseSpecMock);
when(responseMock.bodyToMono(Employee.class))
.thenReturn(Mono.just(mockEmployee));
Mono<Employee> employeeMono = employeeService.getEmployeeById(employeeId);
StepVerifier.create(employeeMono)
.expectNextMatches(employee -> employee.getRole()
.equals(Role.LEAD_ENGINEER))
.verifyComplete();
}
}
4. Использование MockWebServer
MockWebServer, созданный командой Square, представляет собой небольшой веб-сервер, который может принимать HTTP-запросы и отвечать на них.
Взаимодействие с MockWebServer из наших тестовых примеров позволяет нашему коду использовать настоящие HTTP-вызовы к локальной конечной точке. Мы получаем выгоду от тестирования предполагаемых HTTP-взаимодействий и избавляемся от проблем, связанных с насмешкой над сложным плавным клиентом.
Команда Spring рекомендует использовать MockWebServer для написания интеграционных тестов.
4.1. Зависимости MockWebServer
Чтобы использовать MockWebServer, нам нужно добавить зависимости Maven для okhttp и mockwebserver в наш pom.xml:
4.2. Добавление MockWebServer в наш тест
Давайте проверим наш EmployeeService с помощью MockWebServer:
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>4.0.1</version>
<scope>test</scope>
</dependency>
В приведенном выше классе JUnit Test методы setUp и tearDown заботятся о создании и завершении работы MockWebServer.
Следующим шагом является сопоставление порта фактического вызова службы REST с портом MockWebServer.
public class EmployeeServiceMockWebServerTest {
public static MockWebServer mockBackEnd;
@BeforeAll
static void setUp() throws IOException {
mockBackEnd = new MockWebServer();
mockBackEnd.start();
}
@AfterAll
static void tearDown() throws IOException {
mockBackEnd.shutdown();
}
}
Теперь пришло время создать заглушку, чтобы MockWebServer мог отвечать на HttpRequest.
4.3. Заглушение ответа
@BeforeEach
void initialize() {
String baseUrl = String.format("http://localhost:%s",
mockBackEnd.getPort());
employeeService = new EmployeeService(baseUrl);
}
Давайте воспользуемся удобным методом постановки в очередь MockWebServer, чтобы поставить тестовый ответ на веб-сервер в очередь:
Когда фактический вызов API будет сделан из метода getEmployeeById(Integer employeeId) в нашем классе EmployeeService, MockWebServer ответит с заглушкой в очереди.
4.4. Проверка запроса
@Test
void getEmployeeById() throws Exception {
Employee mockEmployee = new Employee(100, "Adam", "Sandler",
32, Role.LEAD_ENGINEER);
mockBackEnd.enqueue(new MockResponse()
.setBody(objectMapper.writeValueAsString(mockEmployee))
.addHeader("Content-Type", "application/json"));
Mono<Employee> employeeMono = employeeService.getEmployeeById(100);
StepVerifier.create(employeeMono)
.expectNextMatches(employee -> employee.getRole()
.equals(Role.LEAD_ENGINEER))
.verifyComplete();
}
Мы также можем убедиться, что MockWebServer был отправлен правильный HttpRequest.
MockWebServer имеет удобный метод с именем takeRequest, который возвращает экземпляр RecordedRequest:
С помощью RecordedRequest мы можем проверить полученный HttpRequest, чтобы убедиться, что наш WebClient отправил его правильно.
5. Заключение
RecordedRequest recordedRequest = mockBackEnd.takeRequest();
assertEquals("GET", recordedRequest.getMethod());
assertEquals("/employee/100", recordedRequest.getPath());
В этом руководстве мы попробовали два основных варианта, доступных для имитации клиентского кода REST на основе WebClient.
Хотя Mockito работает и может быть хорошим вариантом для простых примеров, рекомендуемый подход — использовать MockWebServer.
Как всегда, исходный код этой статьи доступен на GitHub.
«
As always, the source code for this article is available over on GitHub.