Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Контрактное тестирование: Pact и consumer-driven контракты

Контрактное тестирование: Pact против ломающихся интеграций

Pact: consumer-driven контракты, генерация pact-файлов, верификация провайдера, Pact Broker и can-i-deploy перед выкаткой.

Прямой ответ

Контрактное тестирование проверяет, что две стороны интеграции понимают друг друга одинаково, не поднимая обе системы одновременно. В подходе consumer-driven контракт формулирует потребитель: он описывает, какие запросы будет отправлять и какие поля ответа использует. Провайдер затем проверяет, что удовлетворяет этим ожиданиям.

Как это работает в Pact:

  1. Потребитель пишет тест против локального мок-сервера Pact. В тесте задаются запрос и ожидаемая форма ответа.
  2. Pact генерирует файл контракта (.json) со всеми взаимодействиями.
  3. Контракт публикуется в Pact Broker — хранилище контрактов с версионированием.
  4. Провайдер верифицирует контракт: поднимается реальное приложение, Pact проигрывает взаимодействия и сверяет ответы.
  5. Перед выкаткой инструмент can-i-deploy отвечает, совместимы ли конкретные версии потребителя и провайдера.

Ключевая идея: провайдер обязан поддерживать только то, что реально используется потребителями. Поля, которые никто не читает, можно менять свободно. Это противоположность подходу «провайдер публикует полную спецификацию, а потребитель надеется».

# потребитель: тест, который одновременно проверяет код и формирует контракт
from pact import Consumer, Provider, Like, Term

pact = Consumer("billing-ui").has_pact_with(Provider("pricing-api"), port=1235)
pact.start_service()

def test_render_price():
    expected = {
        "price": Like(1500),                      # важно наличие и тип, не значение
        "currency": Term(r"^(RUB|USD)$", "RUB"),  # формат
        "vat_included": True,
    }
    (pact.given("товар 42 доступен")
          .upon_receiving("запрос цены товара 42")
          .with_request(method="GET", path="/items/42/price")
          .will_respond_with(200, body=expected))
    with pact:
        data = fetch_price(item_id=42)
    assert render_price(data) == "1 500 ₽"

Матчеры — важная часть. Like говорит «важен тип, а не значение», Term задаёт регулярное выражение, EachLike — «каждый элемент массива похож на образец». Без матчеров контракт становится хрупким: любое изменение значения ломает верификацию.

Практика: организация процесса

1. Контракт принадлежит потребителю. Изменение ожиданий — это изменение кода потребителя. Провайдер не может «исправить» контракт за потребителя, иначе теряется смысл.

2. Верификация провайдера — обычный тест. Она запускается в CI провайдера и должна падать, если изменение ломает потребителя. Практика: верифицировать контракты всех потребителей, а не только последнего.

3. Pact Broker даёт видимость. Кто от кого зависит, какие версии опубликованы, какие потребители используют удаляемое поле. Без брокера процесс быстро вырождается в ручную переписку.

4. can-i-deploy закрывает главный вопрос. Перед выкаткой версии провайдера инструмент проверяет матрицу совместимости с известными версиями потребителей. Это защищает от классического инцидента: провайдер выкатился, потребители в проде сломались.

5. Состояния провайдера (provider states). Контракт описывает не только запрос, но и требуемое состояние: «товар 42 доступен». При верификации провайдер обязан привести себя в это состояние — обычно через подготовленный набор данных.

6. Pact не заменяет другие тесты. Он не проверяет бизнес-логику, авторизацию и производительность. Его задача — форма взаимодействия. Полноценные интеграционные проверки остаются, см. интеграционные тесты в Docker.

Рекомендации:

  1. Начинайте с одной пары сервисов. Внедрять контрактное тестирование сразу на всех интеграциях — путь к долгому внедрению без результата.
  2. Не описывайте лишнего. Каждое взаимодействие в контракте — обязательство провайдера. Лишние поля превращаются в лишние ограничения.
  3. Различайте Pact и OpenAPI. Спецификация описывает возможное, контракт — фактически используемое. Они дополняют друг друга: о контрактной разработке через спецификацию — в статье про OpenAPI.
  4. Используйте мок Pact как двойник. Тот же контракт даёт реалистичный мок для разработки интерфейса; о видах подмен — в статье про тестовые двойники.
  5. Версионируйте осознанно. Совместимость определяется парой версий, а не одной стороной; поэтому в брокере хранятся обе.

Практический сценарий: удаление поля сломало три сервиса

Контекст: команда провайдера убрала из ответа поле legacy_price, посчитав его неиспользуемым. Через час после выкатки начали падать три потребителя.

  1. Что было. Интеграции проверялись вручную на стенде, где был развёрнут только один потребитель. Об остальных команда провайдера не знала.
  2. Почему не заметили. Спецификация OpenAPI описывала поле как необязательное, поэтому его удаление выглядело совместимым изменением. Фактически два потребителя читали его как обязательное.
  3. Внедрение. (а) Каждый потребитель добавил Pact-тесты на используемые поля и опубликовал контракты в брокер; (б) провайдер добавил верификацию всех контрактов в свой CI; (в) в пайплайн выкатки добавили can-i-deploy, блокирующий релиз при несовместимости.
  4. Проверка. Через месяц провайдер снова попытался удалить поле — уже запланированно. Верификация упала на двух контрактах; команду потребителей уведомили автоматически, удаление отложили до их перехода.
  5. Дополнительно. Брокер показал ещё одну зависимость, о которой не знали обе стороны: внутренний отчёт читал то же поле напрямую из кэша.

Вывод: контрактное тестирование ценно не столько проверкой формы ответа, сколько явной фиксацией зависимостей между командами. Знание о том, кто что использует, предотвращает инциденты раньше, чем тесты.

FAQ

Можно ли обойтись без Pact Broker? Технически да: pact-файлы можно передавать через артефакты CI. Практически это работает только для одной пары сервисов; с ростом числа зависимостей отсутствие централизованного хранилища делает матрицу совместимости непроверяемой.

Что делать, если провайдер не может выполнить контракт? Это и есть цель проверки: контракт должен быть согласован до выкатки. Рабочий процесс — провайдер сообщает о невозможности, потребитель меняет ожидание, контракт обновляется.

Подходит ли Pact для асинхронных сообщений? Да, есть отдельный режим для сообщений: потребитель описывает ожидаемое событие, провайдер подтверждает, что публикует совместимое. Механика та же, но без HTTP-запросов.

Насколько это дорого во внедрении? Основные затраты — не код, а договорённости: кто отвечает за контракты, как согласуются изменения, где хранится брокер. Техническая часть для одной пары сервисов занимает дни, организационная — недели.

Источники: Pact docs, Pact Broker docs, Martin Fowler (consumer-driven contracts) — ссылки в шапке.