Статьи

Лучшие интеграционные тесты с WireMock

Независимо от того, придерживаетесь ли вы классической тестовой пирамиды или одного из более новых подходов, таких как Тестирование Сота, вы должны начать писать интеграционные тесты в какой-то момент во время разработки.

Существуют разные типы интеграционных тестов, которые вы можете написать. Начиная с тестов на постоянство, вы можете проверить взаимодействие между вашими компонентами или смоделировать вызов внешних служб. Эта статья будет о последнем случае.

Давайте начнем с мотивирующего примера, прежде чем говорить о WireMock.

Сервис ChuckNorrisFact

Полный пример можно найти на GitHub .

Возможно, вы видели, как я использовал API фактов Чака Норриса в предыдущем сообщении в блоге . API послужит нам примером для другого сервиса, от которого зависит наша реализация.

У нас есть простой ChuckNorrisFactController в качестве API для ручного тестирования. Рядом с «бизнес-классами» находится ChuckNorrisService который выполняет вызов внешнего API. Он использует Spring RestTemplate . Ничего особенного.

То, что я видел много раз, — это тесты, которые высмеивают шаблон RestTemplate и возвращают какой-то заранее подготовленный ответ. Реализация может выглядеть так:

1
2
3
4
5
6
7
8
9
@Service
public class ChuckNorrisService{
...
  public ChuckNorrisFact retrieveFact() {
    ResponseEntity<ChuckNorrisFactResponse> response = restTemplate.getForEntity(url, ChuckNorrisFactResponse.class);
    return Optional.ofNullable(response.getBody()).map(ChuckNorrisFactResponse::getFact).orElse(BACKUP_FACT);
  }
 ...
 }

Рядом с обычными модульными тестами, проверяющими успешные случаи, будет, по крайней мере, один тест, охватывающий случай ошибки, то есть код состояния 4xx или 5xx:

01
02
03
04
05
06
07
08
09
10
11
12
@Test
  public void shouldReturnBackupFactInCaseOfError() {
    String url = "http://localhost:8080";
    RestTemplate mockTemplate = mock(RestTemplate.class);
    ResponseEntity<ChuckNorrisFactResponse> responseEntity = new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE);
    when(mockTemplate.getForEntity(url, ChuckNorrisFactResponse.class)).thenReturn(responseEntity);
    var service = new ChuckNorrisService(mockTemplate, url);
 
    ChuckNorrisFact retrieved = service.retrieveFact();
 
    assertThat(retrieved).isEqualTo(ChuckNorrisService.BACKUP_FACT);
  }

Не выглядит плохо, верно? Ответный объект возвращает код ошибки 503, и наш сервис не завершится сбоем. Все тесты зеленого цвета, и мы можем развернуть наше приложение.

К сожалению, Spring RestTemplate не работает так. Подпись метода getForEntity дает нам очень маленькую подсказку. Это заявляет, throws RestClientException . И именно в этом случае макет RestTemplate отличается от фактической реализации. Мы никогда не получим ResponseEntity с кодом статуса 4xx или 5xx. RestTemplate будет выбрасывать подкласс
RestClientException . Глядя на иерархию классов, мы можем получить хорошее представление о том, что можно бросить:

Поэтому давайте посмотрим, как мы можем сделать этот тест лучше.

WireMock на помощь

WireMock моделирует веб-службы, запуская фиктивный сервер и возвращая ответы, которые вы настроили для возврата. Его легко интегрировать в ваши тесты, а запросы на макет также просты благодаря хорошему DSL.

Для JUnit 4 есть WireMockRule который помогает запустить остановку сервера. Для JUnit 5 вам придется сделать это самостоятельно. Когда вы проверяете пример проекта, вы можете найти ChuckNorrisServiceIntegrationTest . Это тест SpringBoot, основанный на JUnit 4. Давайте посмотрим на него.

Наиболее важной частью является ClassRule :

1
2
@ClassRule
  public static WireMockRule wireMockRule = new WireMockRule();

Как упоминалось ранее, это запустит и остановит сервер WireMock. Вы также можете использовать правило как обычное Rule для запуска и остановки сервера для каждого теста. Для нашего теста это не обязательно.

Далее вы можете увидеть несколько методов configureWireMockFor... Они содержат инструкции для WireMock, когда возвращать ответ. Разделение конфигурации WireMock на несколько методов и вызов их из тестов — мой подход к использованию WireMock. Конечно, вы можете настроить все возможные запросы в методе @Before . Для успеха мы делаем:

1
2
3
4
5
public void configureWireMockForOkResponse(ChuckNorrisFact fact) throws JsonProcessingException {
    ChuckNorrisFactResponse chuckNorrisFactResponse = new ChuckNorrisFactResponse("success", fact);
    stubFor(get(urlEqualTo("/jokes/random"))
        .willReturn(okJson(OBJECT_MAPPER.writeValueAsString(chuckNorrisFactResponse))));
  }

Все методы импортируются статически из com.github.tomakehurst.wiremock.client.WireMock . Как видите, мы вставляем HTTP GET в путь /jokes/random и возвращаем объект JSON.
okJson() — это просто сокращение для ответа 200 с содержимым JSON. На случай ошибки код еще проще:

1
2
3
4
private void configureWireMockForErrorResponse() {
    stubFor(get(urlEqualTo("/jokes/random"))
        .willReturn(serverError()));
  }

Как видите, DSL облегчает чтение инструкций.

Имея WireMock, мы видим, что наша предыдущая реализация не работает, так как RestTemplate генерирует исключение. Поэтому мы должны скорректировать наш код:

1
2
3
4
5
6
7
8
public ChuckNorrisFact retrieveFact() {
    try {
      ResponseEntity<ChuckNorrisFactResponse> response = restTemplate.getForEntity(url, ChuckNorrisFactResponse.class);
      return Optional.ofNullable(response.getBody()).map(ChuckNorrisFactResponse::getFact).orElse(BACKUP_FACT);
    } catch (HttpStatusCodeException e){
      return BACKUP_FACT;
    }
  }

Это уже охватывает основные варианты использования WireMock. Настройте ответ на запрос, выполните тест, проверьте результаты. Это так просто.

Тем не менее, есть одна проблема, с которой вы обычно сталкиваетесь при запуске тестов в облачной среде. Посмотрим, что мы можем сделать.

WireMock на динамическом порту

Вы могли заметить, что интеграционный тест в проекте содержит
Класс ApplicationContextInitializer и его аннотация @TestPropertySource перезаписывают URL-адрес реального API. Это потому, что я хотел запустить WireMock на случайном порту. Конечно, вы можете настроить фиксированный порт для WireMock и использовать его в качестве жестко заданного значения в своих тестах. Но если ваши тесты выполняются в инфраструктуре некоторых облачных провайдеров, вы не можете быть уверены, что порт свободен. Поэтому я думаю, что случайный порт лучше.

Тем не менее, при использовании свойств в приложении Spring мы должны каким-то образом передавать случайный порт нашему сервису. Или, как вы можете видеть в примере, перезапишите URL. Вот почему мы используем ApplicationContextInitializer . Мы добавляем динамически назначенный порт в контекст приложения, а затем можем ссылаться на него, используя свойство
${wiremock.port} . Единственным недостатком здесь является то, что теперь мы должны использовать ClassRule. Иначе мы не смогли получить доступ к порту до инициализации приложения Spring.

Решив эту проблему, давайте рассмотрим одну распространенную проблему, когда речь идет о HTTP-вызовах.

Таймауты

WireMock предлагает гораздо больше возможностей для ответов, чем просто ответы на запросы GET. Другой тестовый случай, который часто забывают, это тестирование таймаутов. Разработчики, как правило, забывают устанавливать время RestTemplate на RestTemplate или даже на URLConnections . Без тайм-аутов оба будут ждать бесконечное количество времени для ответов. В лучшем случае вы не заметите, в худшем случае все ваши потоки ждут ответа, который никогда не придет.

Поэтому мы должны добавить тест, который имитирует время ожидания. Конечно, мы можем также создать задержку, например, с помощью макета Mockito, но в этом случае мы бы снова догадались, как ведет себя RestTemplate. Имитировать задержку с помощью WireMock довольно просто:

1
2
3
4
5
6
7
private void configureWireMockForSlowResponse() throws JsonProcessingException {
    ChuckNorrisFactResponse chuckNorrisFactResponse = new ChuckNorrisFactResponse("success", new ChuckNorrisFact(1L, ""));
    stubFor(get(urlEqualTo("/jokes/random"))
        .willReturn(
            okJson(OBJECT_MAPPER.writeValueAsString(chuckNorrisFactResponse))
                .withFixedDelay((int) Duration.ofSeconds(10L).toMillis())));
  }

withFixedDelay() ожидает значение int, представляющее миллисекунды. Я предпочитаю использовать Duration или хотя бы константу, которая указывает, что параметр представляет миллисекунды без необходимости каждый раз читать JavaDoc.

После установки тайм-аута на нашем RestTemplate и добавления теста для медленного ответа мы можем видеть, что RestTemplate генерирует ResourceAccessException . Таким образом, мы можем либо настроить блок catch для перехвата этого исключения и HttpStatusCodeException либо просто перехватить суперкласс обоих:

1
2
3
4
5
6
7
8
public ChuckNorrisFact retrieveFact() {
    try {
      ResponseEntity<ChuckNorrisFactResponse> response = restTemplate.getForEntity(url, ChuckNorrisFactResponse.class);
      return Optional.ofNullable(response.getBody()).map(ChuckNorrisFactResponse::getFact).orElse(BACKUP_FACT);
    } catch (RestClientException e){
      return BACKUP_FACT;
    }
  }

Теперь мы рассмотрели наиболее распространенные случаи, когда выполняются HTTP-запросы, и мы можем быть уверены, что тестируем их в реальных условиях.

Почему не журчалка?

Другой выбор для тестирования интеграции HTTP — Hoverfly . Он работает аналогично WireMock, но я предпочел последний. Причина в том, что WireMock также весьма полезен при выполнении сквозных тестов, включающих браузер. Hoverfly (по крайней мере, библиотека Java) ограничен использованием прокси-серверов JVM. Это может сделать его быстрее, чем WireMock, но когда, например, в игру вступает какой-то код JavaScript, он вообще не работает. Тот факт, что WireMock запускает веб-сервер, очень полезен, когда код вашего браузера также напрямую вызывает некоторые другие службы. Затем вы также можете высмеивать их с помощью WireMock и писать, например, свои тесты Selenium.

Вывод

Я надеюсь, что эта статья может показать вам две вещи:

  1. Важность интеграционных тестов
  2. что WireMock довольно хорош

Конечно, обе темы могут заполнить гораздо больше статей. Тем не менее, я хотел дать вам представление о том, как использовать WireMock и на что он способен. Не стесняйтесь проверять их документацию и пробовать еще много вещей. Например, тестирование аутентификации с помощью WireMock также возможно.

Смотрите оригинальную статью здесь: лучшие интеграционные тесты с WireMock

Мнения, высказанные участниками Java Code Geeks, являются их собственными.