Давайте одразу перейдемо до суті. Коли мікросервіси взаємодіють один з одним, вони стають як двоє розробників, які домовилися: "Я тобі відправлю файл у форматі JSON з такими полями!". У контрактному тестуванні цей "договор" називається контрактом. Ідея в тому, що взаємодії між сервісами перевіряються на відповідність заздалегідь заданим умовам, щоб упевнитися, що ніхто випадково не порушив цей "договор".
Приклад: Уявіть, що ви працюєте над мікросервісом "Замовник", який робить HTTP-запити до мікросервісу "Продукти". "Продукти" повертає у відповіді JSON з інформацією про товар. Ви можете написати контрактні тести, щоб гарантувати, що "Продукти" завжди повертає коректний JSON, який відповідає очікуванням "Замовника".
Чому це важливо?
У мікросервісній архітектурі, де кожен сервіс незалежний, але взаємодіє з іншими, оновлення однієї частини системи може легко зламати іншу. Наприклад, якщо команда розробників сервісу "Продукти" раптово змінить структуру JSON-відповіді, то сервіс "Замовник" почне падати з помилками. Контрактне тестування допомагає уникнути таких ситуацій:
- Воно гарантує стабільність взаємодій між сервісами.
- Воно економить час, оскільки проблеми з інтеграцією знаходять на ранніх етапах розробки.
- Воно зменшує кількість ручних перевірок, бо взаємодії тестуються автоматично.
Різниця між інтеграційним і контрактним тестуванням
Інтеграційні тести перевіряють взаємодію кількох компонентів, а контрактні тести фокусуються на точній відповідності очікуваних вхідних і вихідних даних між двома сервісами. Контрактне тестування корисне тим, що дозволяє тестувати взаємодію в ізольованому оточенні, не піднімаючи обидва сервіси.
Основи роботи з Pact
Pact — це популярний інструмент для контрактного тестування. Він слугує посередником між двома сторонами:
- Споживачем (consumer) — сервісом, який надсилає запити.
- Провайдером (provider) — сервісом, який відповідає на запити.
Pact дозволяє створювати і перевіряти контракти між цими сторонами.
Основний принцип роботи Pact
- Створення контракту: споживач створює контракт, який описує, які запити він надсилає і які відповіді очікує від провайдера.
- Перевірка контракту: провайдер перевіряє контракт, щоб гарантувати, що він може правильно відповідати на запити споживача.
Приклад робочого потоку:
- Сервіс "Замовник" генерує контракт, де вказано:
- Я надішлю GET-запит на
/products/1 - Я очікую у відповіді JSON з полями
id,name,price
- Я надішлю GET-запит на
- Цей контракт передається сервісу "Продукти".
- Сервіс "Продукти" використовує Pact для перевірки, що він може відправити такий JSON.
Приклад використання Pact для мікросервісів
Перейдемо до практики. Уявімо, що в нас є два мікросервіси:
- Consumer (споживач): сервіс "Замовник", який запитує інформацію про товари.
- Provider (провайдер): сервіс "Продукти", який повертає інформацію про товари.
Крок 1: Створення контракту на боці споживача
Почнемо зі "Замовника". У цьому сервісі ми описуємо, який запит і відповідь очікуємо від "Продуктів".
Перед початком роботи додамо бібліотеку Pact у наш build.gradle:
dependencies {
testImplementation 'au.com.dius.pact.consumer:junit5:4.5.7'
}
Написання тесту з використанням Pact
@PactTestFor(providerName = "ProductService")
public class ProductConsumerContractTest {
@Pact(consumer = "CustomerService")
public RequestResponsePact createPact(PactDslWithProvider builder) {
return builder
.given("Product with ID 1 exists")
.uponReceiving("A request for product with ID 1")
.path("/products/1")
.method("GET")
.willRespondWith()
.status(200)
.body("{\"id\": 1, \"name\": \"Laptop\", \"price\": 1200.00}")
.toPact();
}
@Test
@PactTestFor(pactMethod = "createPact")
public void testConsumerBehaviour(MockServer mockServer) {
// Використовуємо mockServer для емуляції API "Продуктів"
String response = new RestTemplate().getForObject(mockServer.getUrl() + "/products/1", String.class);
// Перевіряємо, що відповідь відповідає очікуванням
assertEquals("{\"id\": 1, \"name\": \"Laptop\", \"price\": 1200.00}", response);
}
}
Пояснення:
- Ми створюємо контракт за допомогою Pact.
- У контракті вказано, що споживач (CustomerService) робить
GET-запит на/products/1і очікує певну JSON-відповідь. - Pact піднімає
MockServerдля тестування поведінки споживача.
Крок 2: Перевірка контракту на боці провайдера
Тепер передамо контракт у сервіс "Продукти" і перевіримо, що він відповідає очікуванням.
Додамо Pact у провайдерський сервіс:
dependencies {
testImplementation 'au.com.dius.pact.provider:junit5:4.5.7'
}
Перевірка контракту
@Provider("ProductService")
@PactBroker(host = "localhost", port = "9292") // Pact broker для зберігання контрактів
public class ProductProviderContractTest {
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
public void validatePacts(PactVerificationContext context) {
context.verifyInteraction();
}
@State("Product with ID 1 exists")
public void productExists() {
// Налаштовуємо тестову базу даних або моки для стану "Продукт з ID 1 існує"
}
}
Пояснення:
- Ми використовуємо Pact для перевірки контракту.
- У методі
productExistsзадаємо початковий стан (наприклад, додаємо запис у базу). - Pact автоматично перевіряє, що сервіс "Продукти" відповідає контракту.
Переваги використання Pact
- Раннє знаходження проблем: проблеми інтеграції між споживачем і провайдером одразу стають помітні.
- Ізоляція тестів: споживач і провайдер тестуються незалежно.
- Документація взаємодій: контракти служать авто-документацією API.
Висновок теми
Контрактне тестування — це супергерой мікросервісних взаємодій. Інструмент на кшталт Pact допомагає вашим командам спати спокійно, знаючи, що оновлення одного сервісу не зруйнують інший. На наступних лекціях ми заглибимося у практичне використання Pact і напишемо наші перші контрактні тести для взаємодії між мікросервісами.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ