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

Потребительские контракты с Pact

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

1. Обзор

В этой быстрой статье мы рассмотрим концепцию контрактов, управляемых потребителями.

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

Мы также создадим тесты на основе контракта как для клиентского приложения, так и для провайдера.

2. Что такое пакт ?

Используя Pact , мы можем определить ожидания потребителей для данного поставщика (это может быть служба HTTP REST) в форме контракта (отсюда и название библиотеки).

Мы собираемся установить этот контракт, используя DSL, предоставленный Pact . После определения мы можем протестировать взаимодействие между потребителями и поставщиком, используя фиктивный сервис, созданный на основе определенного контракта. Кроме того, мы протестируем службу на соответствие контракту с помощью фиктивного клиента.

3. Зависимость от Maven

Для начала нам нужно добавить зависимость Maven в библиотеку pact-jvm-consumer-junit_2.11 :

<dependency>
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-consumer-junit_2.11</artifactId>
<version>3.5.0</version>
<scope>test</scope>
</dependency>

4. Определение контракта

Когда мы хотим создать тест с использованием Pact , сначала нам нужно определить @Rule , который будет использоваться в нашем тесте:

@Rule
public PactProviderRuleMk2 mockProvider
= new PactProviderRuleMk2("test_provider", "localhost", 8080, this);

Мы передаем имя провайдера, хост и порт, на котором будет запущен макет сервера (который создается из контракта).

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

Первый метод — это запрос GET, который возвращает JSON с двумя полями. Когда запрос выполнен успешно, он возвращает код ответа HTTP 200 и заголовок Content-Type для JSON.

Давайте определим такой контракт, используя Pact .

Нам нужно использовать аннотацию @Pact и передать имя потребителя, для которого определен контракт. Внутри аннотированного метода мы можем определить наш контракт GET:

@Pact(consumer = "test_consumer")
public RequestResponsePact createPact(PactDslWithProvider builder) {
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");

return builder
.given("test GET")
.uponReceiving("GET REQUEST")
.path("/pact")
.method("GET")
.willRespondWith()
.status(200)
.headers(headers)
.body("{\"condition\": true, \"name\": \"tom\"}")
(...)
}

Используя Pact DSL, мы определяем, что для данного запроса GET мы хотим вернуть ответ 200 с определенными заголовками и телом.

Второй частью нашего контракта является метод POST. Когда клиент отправляет запрос POST по пути /pact с правильным телом JSON, он возвращает код ответа HTTP 201.

Определим такой контракт с помощью Pact:

(...)
.given("test POST")
.uponReceiving("POST REQUEST")
.method("POST")
.headers(headers)
.body("{\"name\": \"Michael\"}")
.path("/pact")
.willRespondWith()
.status(201)
.toPact();

Обратите внимание, что нам нужно вызвать метод toPact() в конце контракта, чтобы вернуть экземпляр RequestResponsePact .

4.1. Получившийся Артефакт Пакта

По умолчанию файлы Pact будут созданы в папке target/pacts . Чтобы настроить этот путь, мы можем настроить плагин maven-surefire:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<pact.rootDir>target/mypacts</pact.rootDir>
</systemPropertyVariables>
</configuration>
...
</plugin>

Сборка Maven создаст файл с именем test_consumer-test_provider.json в папке target/mypacts , который содержит структуру запросов и ответов:

{
"provider": {
"name": "test_provider"
},
"consumer": {
"name": "test_consumer"
},
"interactions": [
{
"description": "GET REQUEST",
"request": {
"method": "GET",
"path": "/"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": {
"condition": true,
"name": "tom"
}
},
"providerStates": [
{
"name": "test GET"
}
]
},
{
"description": "POST REQUEST",
...
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.5.0"
}
}
}

5. Тестирование клиента и провайдера с использованием контракта

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

Каждый из этих тестов будет использовать макет своего аналога, основанный на контракте, что означает:

  • клиент будет использовать ложный провайдер
  • провайдер будет использовать фиктивный клиент

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

5.1. Тестирование клиента

Как только мы определили контракт, мы можем протестировать взаимодействие со службой, которая будет создана на основе этого контракта. Мы можем создать обычный тест JUnit, но нам нужно не забыть поставить аннотацию @PactVerification в начале теста.

Напишем тест для GET-запроса:

@Test
@PactVerification()
public void givenGet_whenSendRequest_shouldReturn200WithProperHeaderAndBody() {

// when
ResponseEntity<String> response = new RestTemplate()
.getForEntity(mockProvider.getUrl() + "/pact", String.class);

// then
assertThat(response.getStatusCode().value()).isEqualTo(200);
assertThat(response.getHeaders().get("Content-Type").contains("application/json")).isTrue();
assertThat(response.getBody()).contains("condition", "true", "name", "tom");
}

Аннотация @PactVerification позаботится о запуске службы HTTP. В тесте нам нужно только отправить запрос GET и подтвердить, что наш ответ соответствует контракту.

Добавим тест и для вызова метода POST:

HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
String jsonBody = "{\"name\": \"Michael\"}";

// when
ResponseEntity<String> postResponse = new RestTemplate()
.exchange(
mockProvider.getUrl() + "/create",
HttpMethod.POST,
new HttpEntity<>(jsonBody, httpHeaders),
String.class
);

//then
assertThat(postResponse.getStatusCode().value()).isEqualTo(201);

Как мы видим, код ответа на POST-запрос равен 201 — именно так, как он был определен в контракте Pact .

Поскольку мы использовали аннотацию @PactVerification() , библиотека Pact запускает веб-сервер на основе ранее определенного контракта перед нашим тестовым примером.

5.2. Тестирование провайдера

Второй шаг проверки нашего контракта — создание теста для провайдера с использованием фиктивного клиента на основе контракта.

Реализация нашего поставщика будет управляться этим контрактом в стиле TDD.

В нашем примере мы будем использовать REST API Spring Boot.

Во-первых, чтобы создать наш тест JUnit, нам нужно добавить зависимость pact-jvm-provider-junit_2.11 :

<dependency>
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-provider-junit_2.11</artifactId>
<version>3.5.0</version>
<scope>test</scope>
</dependency>

Это позволяет нам создать тест JUnit, используя PactRunner и указав имя провайдера и расположение артефакта Pact:

@RunWith(PactRunner.class)
@Provider("test_provider")
@PactFolder("pacts")
public class PactProviderTest {
//...
}

Чтобы эта конфигурация работала, мы должны поместить файл test_consumer-test_provider.json в папку pacts нашего проекта службы REST.

Далее мы определим цель, которая будет использоваться для проверки взаимодействий в контракте, и запустим приложение Spring Boot перед запуском тестов:

@TestTarget
public final Target target = new HttpTarget("http", "localhost", 8082, "/spring-rest");

private static ConfigurableWebApplicationContext application;

@BeforeClass
public static void start() {
application = (ConfigurableWebApplicationContext)
SpringApplication.run(MainApplication.class);
}

Наконец, мы укажем состояния в контракте, которые мы хотим протестировать:

@State("test GET")
public void toGetState() { }

@State("test POST")
public void toPostState() { }

Запуск этого класса JUnit выполнит два теста для двух запросов GET и POST. Давайте посмотрим на журнал:

Verifying a pact between test_consumer and test_provider
Given test GET
GET REQUEST
returns a response which
has status code 200 (OK)
includes headers
"Content-Type" with value "application/json" (OK)
has a matching body (OK)

Verifying a pact between test_consumer and test_provider
Given test POST
POST REQUEST
returns a response which
has status code 201 (OK)
has a matching body (OK)

Обратите внимание, что мы не включили сюда код для создания службы REST. Полный сервис и тест можно найти в проекте GitHub .

6. Заключение

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

Мы создали контракт, используя библиотеку Pact . Как только мы определили контракт, мы смогли проверить клиента и службу на соответствие контракту и подтвердить, что они соответствуют спецификации.

Реализацию всех этих примеров и фрагментов кода можно найти в проекте GitHub — это проект Maven, поэтому его должно быть легко импортировать и запускать как есть.