WireMock — быстрый гайд

Описание

WireMock — библиотека для работы со стабами и моками при разработке и тестировании. Создается тестовый HTTP-сервер, при работе с которым можно задавать ожидаемое поведение (что должно происходить), затем вызывать тестируемый сервис и проверять полученный результат.

В основном WireMock применяется в юнит-тестировании и тестировании API, то есть во время этапа интеграционного тестирования продукта. 

Можно считать, что WireMock это симулятор API на базе HTTP, работающий как инструмент виртуализации тестируемых сервисов или, коротко — как мок-сервер

WireMock позволяет проверять API, когда оно еще не существует, или неполное (недописанное) или к внешнему API платный или ограниченный доступ.

WireMock дает возможность тестировать «пограничные ситуации» (edge-кейсы) и подобные режимы работы сервиса. Библиотека быстрая, что иногда значительно сокращает время билда. 

Итак, простым языком, Wiremock — имитатор тестового сервиса для интеграционного тестирования. Разберем WireMock на простом реальном примере.

Как он работает

Например, твой стартап создает некое музыкальное приложение, для поиска и подбора музыкальных семплов, и должна быть функция, которая подключается к внешнему API (типа Spotify или что-то подобное), а он например платный, или ограниченный по количеству запросов. Возникает вопрос, как провести качественное интеграционное тестирование работы нашего приложения с этими внешними API. Тут может быть два подхода.

Первый подход

Первый подход простой и очевидный — тестировать в (сложно настраиваемом) тестовом окружении или даже на проде. Но могут возникнуть следующие проблемы:

  • Зачастую использование API сопряжено с определенными затратами, особенно у очень популярных сервисов.
  • Внешняя система API может быть не всегда гарантированно доступна, то есть получается, мы полностью зависим от внешней системы, и любой временный сбой в этой системе задержит наши тесты, и соответственно релиз.
  • Внешняя система API может иметь неудобную для тестирования конфигурацию, к тому же в музыкальной индустрии могут существовать какие-то правовые ограничения.

Второй подход — моки и стабы WireMock

Или мы просто и быстро проводим запросы через свой мок-сервер, который имитирует ответы.

Важные функции WireMock

1. Стабы: Это техника, которая позволяет настроить (конфигурировать) HTTP-ответ, возвращаемый WireMock-сервером при получении определенного HTTP-запроса. «Заглушить» стабом HTTP-запросы в WireMock можно с помощью статического метода givenThat()класса WireMock.

2. Верификация: WireMock-сервер регистрирует все полученные запросы, пока не будет закрыт. Это позволяет проверить (верифицировать), был ли получен запрос, соответствующий (матчится) определенному паттерну, а также получить подробную информацию о запросах.

3. Запись и воспроизведение взаимодействий: WireMock может создавать маппинги стабов из полученных запросов. В сочетании с функцией проксирования это позволяет записывать маппинги-отображения стабов при работе с API.

4. Имитация сбоев и задержек: Имитаторы (дублеры, фейки) сервисов, среди которых в QA наиболее важны моки и стабы, используются для тестовой имитации ошибочного поведения; настоящий сервис, тем более крупный, нельзя «заставить» неправильно работать, когда тебе нужно тестировать свое приложение.

5. Концепция Stateful Behaviour (моделирование поведения сервиса с сохранением состояния): Большинство веб-сервисов, как правило, имеют набор некоторых стабильных состояний, которые могут меняться при взаимодействии с пользователями. Поэтому тестировщику полезно иметь возможность имитировать эти состояние и их сохранение, когда он имитирует реальный сервис его каким-то тестовым дублем (заменителем).

6. Также активно используется как JVM-библиотека в юнит-тестировании; или запускается как отдельный (standalone) процесс на том же хосте, удаленном сервере или в облаке.

7. Все возможности WireMock легко доступны через его REST (JSON) интерфейс и Java API.

Связка с Maven

Чтобы пользоваться WireMock, понадобится подключить его в POM Maven:

<dependency>
    <groupId>com.github.tomakehurst</groupId>
    <artifactId>wiremock-jre8</artifactId>
    <version>2.33.2</version>
    <scope>test</scope>
</dependency>

Для Java 8 в standalone-режиме:

<dependency>
    <groupId>com.github.tomakehurst</groupId>
    <artifactId>wiremock-jre8-standalone</artifactId>
    <version>2.33.2</version>
    <scope>test</scope>
</dependency>

Зависимости для Gradle

Нужно добавить следующую зависимость в файл build.gradle.

Для Java 8:

testImplementation "com.github.tomakehurst:wiremock-jre8:2.33.2"

Для Java 8 standalone:

testImplementation "com.github.tomakehurst:wiremock-jre8-standalone:2.33.2"

Программно-управляемый сервер (без автоконфигурации JUnit)

Рассмотрим, как сконфигурировать «автономный» WireMock-сервер, то есть не через автоматическую конфигурацию в JUnit. На примере простого стаба.

Настраиваем сервер

Сначала создаем экземпляр WireMock-сервера:

WireMockServer wireMockServer = new WireMockServer(String host, int port);

Если не прописывать аргументы, хостом сервера по дефолту будет localhost, а портом сервера — 8080.

Запуск и стоп сервера, все просто:

wireMockServer.start();

и

wireMockServer.start();

Как работает Wiremock

Сначала посмотрим на работу библиотеки на примере стандартного ее использования, когда создается стаб для проверки точного URL-адреса, без лишних конфигураций.

Инстанциируем сервер:

WireMockServer wireMockServer = new WireMockServer();

Чтобы клиент мог подключиться, сервер должен быть сперва запущен:

wireMockServer.start();

Веб-сервис получает свой стаб:

configureFor("localhost", 8080);
stubFor(get(urlEqualTo("/baeldung")).willReturn(aResponse().withBody("Welcome to Baeldung!")));

Мы здесь используем API Apache HttpClient для создания клиента:

CloseableHttpClient httpClient = HttpClients.createDefault();

Запрос выполнен, ответ получен, все ок:

HttpGet request = new HttpGet("http://localhost:8080/baeldung");
HttpResponse httpResponse = httpClient.execute(request);

Переменная httpResponse преобразуется в String с помощью хелпер-метода:

private String convertResponseToString(HttpResponse response) throws IOException {
    InputStream responseStream = response.getEntity().getContent();
    Scanner scanner = new Scanner(responseStream, "UTF-8");
    String responseString = scanner.useDelimiter("\\Z").next();
    scanner.close();
    return responseString;
}

Здесь использован conversion helper метод:

private String convertResponseToString(HttpResponse response) throws IOException {
    InputStream responseStream = response.getEntity().getContent();
    Scanner scanner = new Scanner(responseStream, "UTF-8");
    String responseString = scanner.useDelimiter("\\Z").next();
    scanner.close();
    return responseString;
}

Ниже мы проверяем, что сервером получен запрос, а его ответ правильный:

verify(getRequestedFor(urlEqualTo("/baeldung")));
assertEquals("Welcome to Baeldung!", stringResponse);

Выполнив свою тестовую задачу, останавливаем WireMock:

wireMockServer.stop();

Работа с WireMock через Rule в JUnit 

Теперь рассмотрим вариант работы через JUnit Rule.

Настраиваем сервер

WireMock-проверки встраиваются в стандартные тест-кейсы JUnit с помощью аннотации @Rule. Через JUnit управлять иногда удобнее, если запускать сервер перед вызовом метода и останавливать после получения ответа.

Как и в первом варианте, WireMock-сервер создается как объект в Java со своим номером порта:

@Rule
public WireMockRule wireMockRule = new WireMockRule(int port);

Когда аргументов нет, будет дефолтный порт сервера, то есть 8080. Хост сервера (по дефолту localhost), и другие параметры можно прописывать в интерфейсе Options.

Проверка URL (URL Matching)

После создания (инстанциирования) WireMockRule далее конфигурируется сам стаб.

Создаем REST-стаб для эндпойнта сервиса с помощью регулярного выражения:

stubFor(get(urlPathMatching("/baeldung/.*"))
  .willReturn(aResponse()
  .withStatus(200)
  .withHeader("Content-Type", "application/json")
  .withBody("\"testing-library\": \"WireMock\"")));

Создаем HTTP-клиент, выполним запрос и получим ответ:

CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet request = new HttpGet("http://localhost:8080/baeldung/wiremock");
HttpResponse httpResponse = httpClient.execute(request);
String stringResponse = convertHttpResponseToString(httpResponse);

В примере кода выше применяется conversion helper-метод.

CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet request = new HttpGet("http://localhost:8080/baeldung/wiremock");
HttpResponse httpResponse = httpClient.execute(request);
String stringResponse = convertHttpResponseToString(httpResponse);

А он вызывает другой private-метод:

private String convertInputStreamToString(InputStream inputStream) {
    Scanner scanner = new Scanner(inputStream, "UTF-8");
    String string = scanner.useDelimiter("\\Z").next();
    scanner.close();
    return string;
}

Проверяем, как работает стаб:

verify(getRequestedFor(urlEqualTo("/baeldung/wiremock")));
assertEquals(200, httpResponse.getStatusLine().getStatusCode());
assertEquals("application/json", httpResponse.getFirstHeader("Content-Type").getValue());
assertEquals("\"testing-library\": \"WireMock\"", stringResponse);

Проверка заголовка запроса

Далее посмотрим, как сделать стаб REST API для матчинга (сопоставления) заголовков.

Настраиваем стаб:

stubFor(get(urlPathEqualTo("/baeldung/wiremock"))
  .withHeader("Accept", matching("text/.*"))
  .willReturn(aResponse()
  .withStatus(503)
  .withHeader("Content-Type", "text/html")
  .withBody("!!! Service Unavailable !!!")));

Как и ранее, работаем по протоколу HTTP — API HttpClient, используя те же helper-методы:

CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet request = new HttpGet("http://localhost:8080/baeldung/wiremock");
request.addHeader("Accept", "text/html");
HttpResponse httpResponse = httpClient.execute(request);
String stringResponse = convertHttpResponseToString(httpResponse);

Ниже верификации и ассерты подтверждают правильную работу стаба:

verify(getRequestedFor(urlEqualTo("/baeldung/wiremock")));
assertEquals(503, httpResponse.getStatusLine().getStatusCode());
assertEquals("text/html", httpResponse.getFirstHeader("Content-Type").getValue());
assertEquals("!!! Service Unavailable !!!", stringResponse);

Проверка тела запросов

В WireMock также можно создавать стабы для REST API с проверкой тела запроса.

Пример конфигурации:

stubFor(post(urlEqualTo("/baeldung/wiremock"))
  .withHeader("Content-Type", equalTo("application/json"))
  .withRequestBody(containing("\"testing-library\": \"WireMock\""))
  .withRequestBody(containing("\"creator\": \"Tom Akehurst\""))
  .withRequestBody(containing("\"website\": \"wiremock.org\""))
  .willReturn(aResponse()
  .withStatus(200)));

Далее создаем объект StringEntity, его будем использовать как тело запроса:

InputStream jsonInputStream 
  = this.getClass().getClassLoader().getResourceAsStream("wiremock_intro.json");
String jsonString = convertInputStreamToString(jsonInputStream);
StringEntity entity = new StringEntity(jsonString);

Выше применяется один из conversion helper-методов, описанных ранее, convertInputStreamToString.

Файл wiremock_intro.json в classpath:

{
    "testing-library": "WireMock",
    "creator": "Tom Akehurst",
    "website": "wiremock.org"
}

Конфигурируем и запускаем HTTP-запросы и ответы:

CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost request = new HttpPost("http://localhost:8080/baeldung/wiremock");
request.addHeader("Content-Type", "application/json");
request.setEntity(entity);
HttpResponse response = httpClient.execute(request);

Валидация стаба:

verify(postRequestedFor(urlEqualTo("/baeldung/wiremock"))
  .withHeader("Content-Type", equalTo("application/json")));
assertEquals(200, response.getStatusLine().getStatusCode());

Приоритеты стабов

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

Посмотрим, как запрос одновременно работает с двумя стабами, с установленным приоритетом и без.

В обоих сценариях будет использоваться следующий приватный helper-метод:

private HttpResponse generateClientAndReceiveResponseForPriorityTests() throws IOException {
    CloseableHttpClient httpClient = HttpClients.createDefault();
    HttpGet request = new HttpGet("http://localhost:8080/baeldung/wiremock");
    request.addHeader("Accept", "text/xml");
    return httpClient.execute(request);
}

Сначала конфигурируем два стаба без приоритета:

stubFor(get(urlPathMatching("/baeldung/.*"))
  .willReturn(aResponse()
  .withStatus(200)));
stubFor(get(urlPathEqualTo("/baeldung/wiremock"))
  .withHeader("Accept", matching("text/.*"))
  .willReturn(aResponse()
  .withStatus(503)));

Далее создаем HTTP-клиент и выполняем запрос через helper-метод:

HttpResponse httpResponse = generateClientAndReceiveResponseForPriorityTests();

Ниже проверяем, что срабатывает стаб, добавленный позже:

verify(getRequestedFor(urlEqualTo("/baeldung/wiremock")));
assertEquals(503, httpResponse.getStatusLine().getStatusCode());

Теперь пробуем задавать приоритеты (чем меньше atPriority, тем выше приоритет):

stubFor(get(urlPathMatching("/baeldung/.*"))
  .atPriority(1)
  .willReturn(aResponse()
  .withStatus(200)));
stubFor(get(urlPathEqualTo("/baeldung/wiremock"))
  .atPriority(2)
  .withHeader("Accept", matching("text/.*"))
  .willReturn(aResponse()
  .withStatus(503)));

Создаем и выполняем запрос:

HttpResponse httpResponse = generateClientAndReceiveResponseForPriorityTests();

Проверяем, что приоритеты правильно работают:

verify(getRequestedFor(urlEqualTo("/baeldung/wiremock")));
assertEquals(200, httpResponse.getStatusLine().getStatusCode());

Источники: 1,2

АйТи Собеc — ты справишься

Какой была ваша первая зарплата в QA и как вы искали первую работу?

Мега обсуждение в нашем телеграм-канале о поиске первой работы. Обмен опытом и мнения.

Подписаться
Уведомить о
guest

0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии

Мы в Telegram

Наш официальный канал
Полезные материалы и тесты
Готовимся к собеседованию
Project- и Product-менеджмент

? Популярное

? Telegram-обсуждения

Наши подписчики обсуждают, как искали первую работу в QA. Некоторые ищут ее прямо сейчас.
Наши подписчики рассказывают о том, как не бояться задавать тупые вопросы и чувствовать себя уверенно в новой команде.
Обсуждаем, куда лучше податься - в менеджмент или по технической ветке?
Говорим о конфликтных ситуациях в команде и о том, как их избежать
$1100*
медианная зарплата в QA в июне 2023

*по результатам опроса QA-инженеров в нашем телеграм-канале

Собеседование

19%*
IT-специалистов переехало или приняло решение о переезде из России по состоянию на конец марта 2022

*по результатам опроса в нашем телеграм-канале

live

Обсуждают сейчас