JavaRush /Курси /Модуль 5. Spring /Лекція 267: Практика: написання контрактних тестів для вз...

Лекція 267: Практика: написання контрактних тестів для взаємодії мікросервісів

Модуль 5. Spring
Рівень 21 , Лекція 6
Відкрита

Уяви, що в тебе є два мікросервіси: сервіс "Покупець" і сервіс "Товар". Перший хоче дізнатись інформацію про товари у другого. Здається, усе просто, але що станеться, якщо команда, яка працює над сервісом "Товар", вирішить змінити структуру повернутих JSON-об'єктів? Так, сервіс "Покупець" почне радісно падати. Щоб цього не сталося, ми використовуємо контракти, які дають змогу переконатися, що взаємодія між сервісами лишається "на одній хвилі".


Що таке Pact?

Pact — це фреймворк для контрактного тестування, що допомагає гарантувати, що два мікросервіси (або клієнт/сервер) взаємодіють так, як вони пообіцяли один одному. Pact працює за принципом "споживач-провайдер":

  • Споживач (Consumer) створює контракт, що описує, як він очікує, що провайдер має себе поводити.
  • Провайдер (Provider) перевіряє, що його функціонал відповідає контракту, і оновлює контракт у разі змін.

Приклад простої схеми Pact:

[Сервіс споживача -> Генерує контракт -> Контракт зберігається -> Провайдер перевіряє контракт]

Задача: Реалізуємо контрактне тестування

У нашому прикладі буде:

  1. Споживач — мікросервіс CustomerService, який викликає API сервісу ProductService.
  2. Провайдер — мікросервіс 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());
    }
}

Що тут відбувається:

  1. У методі createPact ми описуємо контракт:
    • За умови (given), що товар з ID 1 існує
    • Якщо клієнт запитує /products/1, поверни JSON-об'єкт з даними про товар
  2. У тесті 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: Запустимо все разом

  1. Підніміть ProductService на localhost:8080. Переконайтеся, що він повертає коректні дані за запитом /products/1.
  2. Спочатку виконайте тест CustomerServicePactTest, щоб згенерувати контракт.
  3. Потім виконайте ProductServicePactTest, щоб перевірити провайдера.

Обробка помилок і типові проблеми

  1. Модифікація API провайдера: якщо команда ProductService модифікує API, тести провайдера почнуть падати. Це сигнал — треба оновити контракт і узгодити зміни зі споживачем.
  2. Невідповідність даних у контракті: якщо провайдер повертає додаткові поля, це не обов'язково помилка, але їх варто обговорити з командою споживача.
  3. Кілька контрактів: якщо ProductService обслуговує кілька споживачів, контракти кожного з них треба перевіряти окремо.

Навіщо все це потрібно на практиці?

Контрактне тестування допомагає знаходити проблеми інтеграції між мікросервісами ще до релізу в production. Це особливо важливо в командах, які працюють над різними сервісами, коли зміни на одній стороні можуть непомітно "поламати" іншу сторону.

Контракти також чудово підходять для CI/CD систем: тести можна запускати як частину пайплайна збірки, щоб автоматизувати перевірку змін.


Вітаю! Тепер ти вмієш писати контрактні тести на Pact. Це важливий крок до якісних і надійних мікросервісних систем. Хай живуть стабільні інтеграції!

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ