Перейти к основному содержимому

Насмешка над веб-клиентом в Spring

· 4 мин. чтения

1. Обзор

В наши дни мы рассчитываем вызывать REST API в большинстве наших сервисов. Spring предоставляет несколько вариантов для создания REST-клиента, рекомендуется использовать WebClient .

В этом кратком руководстве мы узнаем, как проводить модульное тестирование сервисов, использующих WebClient для вызова API .

2. Насмешка

У нас есть два основных варианта мока в наших тестах:

  • Используйте Mockito , чтобы имитировать поведение WebClient
  • Используйте WebClient по-настоящему, но имитируйте службу, которую он вызывает, используя MockWebServer (okhttp)

3. Использование Мокито

Mockito — самая распространенная библиотека для создания макетов для Java. Это хорошо для предоставления предопределенных ответов на вызовы методов, но все становится сложнее, когда они имитируют плавные API. Это связано с тем, что в свободном API между вызывающим кодом и макетом передается множество объектов.

Например, пусть класс EmployeeService с методом getEmployeeById извлекает данные через HTTP с помощью 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);
}
}

Мы можем использовать Mockito, чтобы издеваться над этим:

@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();
}

}

Как мы видим, нам нужно предоставить другой фиктивный объект для каждого вызова в цепочке с четырьмя различными требуемыми вызовами when / thenReturn . Это многословно и громоздко . Это также требует от нас знания деталей реализации того, как именно наш сервис использует WebClient, что делает этот способ тестирования ненадежным.

Итак, как мы можем написать лучшие тесты для WebClient?

4. Использование MockWebServer

MockWebServer , созданный командой Square, представляет собой небольшой веб-сервер, который может получать HTTP-запросы и отвечать на них.

Взаимодействие с MockWebServer из наших тестовых случаев позволяет нашему коду использовать настоящие HTTP-вызовы к локальной конечной точке . Мы получаем преимущество от тестирования предполагаемых HTTP-взаимодействий и избавляемся от проблем, связанных с имитацией сложного плавного клиента.

** Команда Spring ** рекомендует использовать MockWebServer для написания интеграционных тестов . ``

4.1. Зависимости MockWebServer

Чтобы использовать MockWebServer , нам нужно добавить зависимости Maven для okhttp и mockwebserver в наш pom.xml:

<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>

4.2. Добавление MockWebServer в наш тест

Давайте протестируем наш EmployeeService с помощью 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();
}
}

В приведенном выше классе JUnit Test методы setUp и tearDown заботятся о создании и завершении работы MockWebServer.

Следующим шагом является сопоставление порта фактического вызова службы REST с портом MockWebServer :

@BeforeEach
void initialize() {
String baseUrl = String.format("http://localhost:%s",
mockBackEnd.getPort());
employeeService = new EmployeeService(baseUrl);
}

Теперь пришло время создать заглушку, чтобы MockWebServer мог отвечать на HttpRequest .

4.3. Заглушение ответа

Давайте воспользуемся удобным методом постановки в очередь MockWebServer , чтобы поставить в очередь тестовый ответ на веб-сервере:

@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();
}

Когда фактический вызов API выполняется из метода getEmployeeById(Integer employeeId) в нашем классе EmployeeService , MockWebServer ответит заглушкой из очереди .

4.4. Проверка запроса

Мы также можем убедиться, что MockWebServer был отправлен правильный HttpRequest .

В MockWebServer есть удобный метод takeRequest , который возвращает экземпляр RecordedRequest :

RecordedRequest recordedRequest = mockBackEnd.takeRequest();

assertEquals("GET", recordedRequest.getMethod());
assertEquals("/employee/100", recordedRequest.getPath());

С помощью RecordedRequest мы можем проверить полученный HttpRequest , чтобы убедиться, что наш WebClient отправил его правильно .

5. Вывод

В этой статье мы продемонстрировали два основных варианта имитации клиентского кода REST на основе WebClient `` .

Хотя Mockito работает и может быть хорошим вариантом для простых примеров, рекомендуемый подход — использовать MockWebServer .

Как всегда, исходный код этой статьи доступен на GitHub.