Прямой ответ
Контрактное тестирование проверяет, что две стороны интеграции понимают друг друга одинаково, не поднимая обе системы одновременно. В подходе consumer-driven контракт формулирует потребитель: он описывает, какие запросы будет отправлять и какие поля ответа использует. Провайдер затем проверяет, что удовлетворяет этим ожиданиям.
Как это работает в Pact:
- Потребитель пишет тест против локального мок-сервера Pact. В тесте задаются запрос и ожидаемая форма ответа.
- Pact генерирует файл контракта (
.json) со всеми взаимодействиями. - Контракт публикуется в Pact Broker — хранилище контрактов с версионированием.
- Провайдер верифицирует контракт: поднимается реальное приложение, Pact проигрывает взаимодействия и сверяет ответы.
- Перед выкаткой инструмент
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.
Рекомендации:
- Начинайте с одной пары сервисов. Внедрять контрактное тестирование сразу на всех интеграциях — путь к долгому внедрению без результата.
- Не описывайте лишнего. Каждое взаимодействие в контракте — обязательство провайдера. Лишние поля превращаются в лишние ограничения.
- Различайте Pact и OpenAPI. Спецификация описывает возможное, контракт — фактически используемое. Они дополняют друг друга: о контрактной разработке через спецификацию — в статье про OpenAPI.
- Используйте мок Pact как двойник. Тот же контракт даёт реалистичный мок для разработки интерфейса; о видах подмен — в статье про тестовые двойники.
- Версионируйте осознанно. Совместимость определяется парой версий, а не одной стороной; поэтому в брокере хранятся обе.
Практический сценарий: удаление поля сломало три сервиса
Контекст: команда провайдера убрала из ответа поле legacy_price, посчитав его неиспользуемым. Через час после выкатки начали падать три потребителя.
- Что было. Интеграции проверялись вручную на стенде, где был развёрнут только один потребитель. Об остальных команда провайдера не знала.
- Почему не заметили. Спецификация OpenAPI описывала поле как необязательное, поэтому его удаление выглядело совместимым изменением. Фактически два потребителя читали его как обязательное.
- Внедрение. (а) Каждый потребитель добавил Pact-тесты на используемые поля и опубликовал контракты в брокер; (б) провайдер добавил верификацию всех контрактов в свой CI; (в) в пайплайн выкатки добавили
can-i-deploy, блокирующий релиз при несовместимости. - Проверка. Через месяц провайдер снова попытался удалить поле — уже запланированно. Верификация упала на двух контрактах; команду потребителей уведомили автоматически, удаление отложили до их перехода.
- Дополнительно. Брокер показал ещё одну зависимость, о которой не знали обе стороны: внутренний отчёт читал то же поле напрямую из кэша.
Вывод: контрактное тестирование ценно не столько проверкой формы ответа, сколько явной фиксацией зависимостей между командами. Знание о том, кто что использует, предотвращает инциденты раньше, чем тесты.
FAQ
Можно ли обойтись без Pact Broker? Технически да: pact-файлы можно передавать через артефакты CI. Практически это работает только для одной пары сервисов; с ростом числа зависимостей отсутствие централизованного хранилища делает матрицу совместимости непроверяемой.
Что делать, если провайдер не может выполнить контракт? Это и есть цель проверки: контракт должен быть согласован до выкатки. Рабочий процесс — провайдер сообщает о невозможности, потребитель меняет ожидание, контракт обновляется.
Подходит ли Pact для асинхронных сообщений? Да, есть отдельный режим для сообщений: потребитель описывает ожидаемое событие, провайдер подтверждает, что публикует совместимое. Механика та же, но без HTTP-запросов.
Насколько это дорого во внедрении? Основные затраты — не код, а договорённости: кто отвечает за контракты, как согласуются изменения, где хранится брокер. Техническая часть для одной пары сервисов занимает дни, организационная — недели.
Источники: Pact docs, Pact Broker docs, Martin Fowler (consumer-driven contracts) — ссылки в шапке.