- Описание
- Как он работает
- Важнейшие функции
- Зависимости Maven и Gradle
- Ручная настройка и работа мок-сервера
- Через JUnit
Описание
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());