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

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

Модуль 5. Spring
27 уровень , 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 обслуживает несколько потребителей, контракты каждого из них должны проверяться отдельно.

Зачем всё это нужно на практике?

Контрактное тестирование помогает обнаруживать проблемы интеграции между микросервисами ещё до выхода в продакшн. Это особенно важно в командах, работающих над разными сервисами, когда изменения на одной стороне могут незаметно "сломать" другую сторону.

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


Поздравляю! Теперь вы умеете писать контрактные тесты на Pact. Это важный шаг к качественным и надёжным микросервисным системам. Да здравствуют стабильные интеграции!

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ