Представьте, что у вас есть два микросервиса: сервис "Покупатель" и сервис "Товар". Первый хочет узнать информацию о товарах у второго. Вроде бы всё просто, но что произойдёт, если команда, работающая над сервисом "Товар", решит поменять структуру возвращаемых JSON-объектов? Да, сервис "Покупатель" начнёт радостно падать. Чтобы этого не происходило, мы используем контракты, которые позволяют убедиться, что взаимодействие между сервисами остаётся "на одной волне".
Что такое Pact?
Pact — это фреймворк для контрактного тестирования, помогающий гарантировать, что два микросервиса (или клиент/сервер) взаимодействуют так, как они обещали друг другу. Pact работает по принципу "потребитель-создатель":
- Потребитель (Consumer) создаёт контракт, описывающий, как он ожидает, что провайдер должен себя вести.
- Провайдер (Provider) проверяет, что его функционал соответствует контракту, и обновляет контракт в случае изменений.
Пример простой схемы Pact:
[Потребитель сервис -> Генерирует контракт -> Контракт хранится -> Провайдер проверяет контракт]
Задача: Реализуем контрактное тестирование
Для нашего примера у нас будет:
- Потребитель — микросервис
CustomerService, который вызывает API сервисаProductService. - Провайдер — микросервис
ProductService, который предоставляет данные о товарах.
Цель: Написать тесты для CustomerService и проверить, что ProductService соответствует контракту.
Шаг 1: Подготовка окружения
Для работы с Pact в Gradle добавим следующие зависимости:
dependencies {
testImplementation 'au.com.dius.pact.provider:junit5:4.6.2'
testImplementation 'au.com.dius.pact.consumer:junit5:4.6.2'
}
Теперь у нас есть всё для работы с Pact на стороне и потребителя, и провайдера.
Шаг 2: Напишем контракт для потребителя
Создадим тестовый класс CustomerServicePactTest, где будем проверять взаимодействие с ProductService.
import au.com.dius.pact.consumer.Pact;
import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
import au.com.dius.pact.consumer.junit5.PactConsumerTestExt;
import au.com.dius.pact.consumer.junit5.PactTestFor;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(PactConsumerTestExt.class)
public class CustomerServicePactTest {
@Pact(consumer = "CustomerService", provider = "ProductService")
public Map<String, Object> createPact(PactDslWithProvider builder) {
return builder
.given("Товар с ID 1 существует")
.uponReceiving("запрос на получение информации о товаре")
.path("/products/1")
.method("GET")
.willRespondWith()
.status(200)
.body("{\"id\": 1, \"name\": \"Laptop\", \"price\": 999.99}")
.toPact();
}
@Test
@PactTestFor(providerName = "ProductService", port = "8080")
void testGetProduct() {
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.getForEntity("http://localhost:8080/products/1", String.class);
assertEquals(200, response.getStatusCodeValue());
assertEquals("{\"id\": 1, \"name\": \"Laptop\", \"price\": 999.99}", response.getBody());
}
}
Что здесь происходит:
- В методе
createPactмы описываем контракт:- При условии (given), что Товар с ID 1 существует
- Если клиент запрашивает
/products/1, верните JSON-объект с данными о товаре
- В тесте
testGetProductмы проверяем, чтоCustomerServiceправильно отправляет запрос и получает ожидаемый ответ.
Этот тест генерирует контракт, который записывается как файл JSON.
Пример контракта:
{
"provider": { "name": "ProductService" },
"consumer": { "name": "CustomerService" },
"interactions": [
{
"description": "запрос на получение информации о товаре",
"request": {
"method": "GET",
"path": "/products/1"
},
"response": {
"status": 200,
"body": { "id": 1, "name": "Laptop", "price": 999.99 }
}
}
]
}
Шаг 3: Проверяем контракт на стороне провайдера
Теперь переходим к ProductService. Здесь мы убедимся, что API этого сервиса соответствует контракту.
Создаем тестовый класс ProductServicePactTest:
import au.com.dius.pact.provider.junit5.PactVerificationContext;
import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(PactVerificationInvocationContextProvider.class)
public class ProductServicePactTest {
@BeforeEach
void before(PactVerificationContext context) {
context.setTarget(new HttpTestTarget("localhost", 8080));
}
@Test
void pactVerificationTest(PactVerificationContext context) {
context.verifyInteraction();
}
}
Основное здесь:
HttpTestTargetуказывает, где находится API нашего провайдера (localhost:8080).- Метод
verifyInteractionзагружает контракт, который мы сгенерировали для потребителя, и проверяет, чтоProductServiceсоответствует описанным взаимодействиям.
Шаг 4: Запустим всё вместе
- Поднимите
ProductServiceнаlocalhost:8080. Убедитесь, что он возвращает корректные данные по запросу/products/1. - Сначала выполните тест
CustomerServicePactTestдля генерации контракта. - Затем выполните
ProductServicePactTest, чтобы проверить провайдера.
Обработка ошибок и типичные проблемы
- Модификация API провайдера: если команда
ProductServiceмодифицирует API, тесты провайдера начнут падать. Это сигнал — нужно обновить контракт и согласовать изменения с потребителем. - Несоответствие данных в контракте: если провайдер возвращает дополнительные поля, это не обязательно ошибка, но их следует обсудить с командой потребителя.
- Множественные контракты: если
ProductServiceобслуживает несколько потребителей, контракты каждого из них должны проверяться отдельно.
Зачем всё это нужно на практике?
Контрактное тестирование помогает обнаруживать проблемы интеграции между микросервисами ещё до выхода в продакшн. Это особенно важно в командах, работающих над разными сервисами, когда изменения на одной стороне могут незаметно "сломать" другую сторону.
Контракты также отлично подходят для CI/CD систем: тесты можно запускать как часть пайплайна сборки, чтобы автоматизировать проверку изменений.
Поздравляю! Теперь вы умеете писать контрактные тесты на Pact. Это важный шаг к качественным и надёжным микросервисным системам. Да здравствуют стабильные интеграции!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ