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