Уяви, що в тебе є два мікросервіси: сервіс "Покупець" і сервіс "Товар". Перший хоче дізнатись інформацію про товари у другого. Здається, усе просто, але що станеться, якщо команда, яка працює над сервісом "Товар", вирішить змінити структуру повернутих 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обслуговує кілька споживачів, контракти кожного з них треба перевіряти окремо.
Навіщо все це потрібно на практиці?
Контрактне тестування допомагає знаходити проблеми інтеграції між мікросервісами ще до релізу в production. Це особливо важливо в командах, які працюють над різними сервісами, коли зміни на одній стороні можуть непомітно "поламати" іншу сторону.
Контракти також чудово підходять для CI/CD систем: тести можна запускати як частину пайплайна збірки, щоб автоматизувати перевірку змін.
Вітаю! Тепер ти вмієш писати контрактні тести на Pact. Це важливий крок до якісних і надійних мікросервісних систем. Хай живуть стабільні інтеграції!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ