Привет, Хабр!
Идея consumer‑driven контрактов старше, чем кажется. Ещё в 2006 году Иэн Робинсон опубликовал на martinfowler.com заметку «Consumer‑Driven Contracts», где предложил простую вещь: пусть требования к API формулирует тот, кто им пользуется, а не тот, кто его пишет. Семь лет спустя команда realestate в Мельбурне разгребала интеграции между свежими Ruby‑микросервисами, устала от бесконечных совместных прогонов и собрала себе библиотеку, которая записывала ожидания клиента в файл и проигрывала их против сервера.
Так появился Pact.
С тех пор он оброс реализациями почти под всё живое: JavaScript, Java, Kotlin, Scala, Python, Go,.NET, Ruby, PHP, Swift, Rust. Внутри у современных клиентов, кстати, лежит одно и то же ядро на Rust, к которому языки цепляются через FFI: поддерживать десять независимых реализаций матчинга оказалось невозможно. Pact Broker остался открытым, коммерческая надстройка Pactflow с 2022 года живёт под крылом SmartBear и в 2026-м продаётся уже как SmartBear Contract Testing.
Дыра между модульными и E2E тестами
Проблема, которую решает contract testing, знакома любому, кто пилил монолит на сервисы. Модульные тесты быстрые, но они ничего не знают про чужие API: клиент замокан, мок отвечает так, как разработчик его когда‑то научил, и живёт своей жизнью. E2E‑тесты знают про всё, но требуют поднять половину компании в одном окружении, растут по количеству связей быстрее, чем по количеству сервисов, и ломаются от чиха.
Между этими двумя слоями остаётся та граница, где происходит основная часть кросс‑командных поломок: поставщик переименовал поле, поменял тип, вернул 204 вместо 200 с пустым телом. У поставщика все тесты зелёные. У потребителя тоже зелёные, потому что мок в его тестах остался старым. Узнают об этом на стейдже, в лучшем случае, или в проде, если стейдж «примерно как прод».
Contract testing целится сюда. Он проверяет совместимость двух сервисов, не запуская их одновременно.
При этом контрактные проверки не заменяют остальные уровни тестирования.
О том, как выстроить весь процесс — от анализа требований до завершения проверок, — читайте в статье «Процесс тестирования: от анализа до завершения».
А разобраться, какие метрики дефектов действительно помогают контролировать качество, можно в материале «6 ошибок в метриках дефектов, из-за которых QA теряет контроль над качеством».
Кто кому что должен
Ролей две. Потребитель (consumer) описывает, чего он ждёт от API: какие запросы шлёт, какие ответы предполагает получить, какие поля из ответа реально читает. Поставщик (provider) берёт это описание и доказывает, что умеет так отвечать.
Описание формализуется в контракт, обычный JSON‑файл с записанными взаимодействиями. Потребитель гоняет свою логику против мока, поднятого из этого контракта. Поставщик отдельно, в своём пайплайне, в своё время, проигрывает записанные запросы против настоящего сервиса и сверяет ответы. Никакого общего окружения, никакой синхронизации релизов.
Основное свойство consumer‑driven подхода: контракт покрывает только то, что потребитель действительно потребляет. API отдаёт тридцать полей, фронт читает четыре, в контракте будут четыре. Поставщик волен добавлять поля, менять их порядок, переписывать внутренности хоть с нуля. Красным контракт станет только тогда, когда изменение задевает то, чем кто‑то пользуется: удалили поле, сменили тип, поменяли код ответа.
Обратная сторона тоже понятна. Pact не говорит вам, правильно ли работает API. Он говорит только, договорились ли две конкретные стороны. Функциональное тестирование поставщика этим не заменяется.
Первый consumer‑тест
Возьмём минимальный сценарий: фронтенд orders-ui ходит в orders-api за конкретным заказом. Пишем на @pact-foundation/pact через интерфейс PactV3 и MatchersV3, это рекомендуемый путь для новых тестов.
const { PactV3, MatchersV3 } = require('@pact-foundation/pact'); const path = require('path'); const { getOrder } = require('./orderClient'); const { like, integer, string } = MatchersV3; const provider = new PactV3({ consumer: 'orders-ui', provider: 'orders-api', dir: path.resolve(process.cwd(), 'pacts'), }); describe('Orders API consumer', () => { it('returns order by id', () => { provider .given('order 42 exists for user alice') .uponReceiving('a request for order 42') .withRequest({ method: 'GET', path: '/orders/42', headers: { Accept: 'application/json' }, }) .willRespondWith({ status: 200, headers: { 'Content-Type': 'application/json' }, body: like({ id: integer(42), status: string('paid'), total: integer(9990), currency: string('RUB'), }), }); return provider.executeTest(async (mockServer) => { const order = await getOrder(mockServer.url, 42); expect(order.id).toBe(42); expect(order.status).toBe('paid'); }); }); });
Обратите внимание на матчеры like, integer, string. Они фиксируют типы, а не значения. Поставщику не нужно возвращать именно 9990 в поле total, достаточно любого целого. Значения в моке существуют исключительно ради самого потребителя: чтобы было на чём прогнать парсинг, рендер и обработку ошибок. Реальные данные придут из реальной базы.
Как только вы начинаете сравнивать конкретные значения, контракт превращается в тест данных поставщика, и первый же сеанс работы с тестовой БД у соседней команды красит ваш пайплайн. Точное значение уместно только там, где оно семантически важно: код валюты, enum статуса, конкретный HTTP‑код. Всё остальное описывается типом, регуляркой (regex), диапазоном или eachLike для массивов.
Строка .given('order 42 exists for user alice') объявляет provider state, то есть состояние, в котором поставщик обязан вести себя описанным образом. В контракт она попадёт просто меткой, а поставщик по этой метке подготовит данные перед проигрыванием запроса.
Провайдерные состояния лучше держать в узде. Формулировка вроде «order 42 exists for user alice» плохо масштабируется: через полгода у вас будет полторы сотни уникальных строк, половина из которых отличается идентификатором. В PactV3 есть параметризованные состояния, и это заметно чище:
.given('an order exists', { id: 42, status: 'paid' })
Поставщик получит id и status как аргументы своего обработчика и засеет базу тем, что попросили. Одна метка вместо семейства почти одинаковых.
После прогона в каталоге pacts/ появится файл orders-ui-orders-api.json. Это и есть контракт.
Верификация на стороне поставщика
Поставщик берёт контракт из брокера и доказывает, что действительно так умеет.
const { Verifier } = require('@pact-foundation/pact'); describe('Orders API provider', () => { it('satisfies all consumer contracts', () => { return new Verifier({ provider: 'orders-api', providerBaseUrl: 'http://localhost:3000', pactBrokerUrl: process.env.PACT_BROKER_URL, pactBrokerToken: process.env.PACT_BROKER_TOKEN, publishVerificationResult: true, providerVersion: process.env.GIT_SHA, providerVersionBranch: process.env.GIT_BRANCH, consumerVersionSelectors: [ { mainBranch: true }, { deployedOrReleased: true }, ], enablePending: true, includeWipPactsSince: '2026-01-01', stateHandlers: { 'an order exists': async ({ id, status }) => { await db.orders.insert({ id, userId: 1, status, total: 9990 }); return { description: `order ${id} seeded` }; }, }, }).verifyProvider(); }); });
Раздел
stateHandlers— это функции подготовки данных, по одной на каждое объявленное состояние. Вставить строку в базу, замокать внешнюю зависимость, включить feature flag. Верификатор дождётся завершения функции и только потом отправит запрос.consumerVersionSelectorsотвечает на вопрос «какие версии контрактов вообще проверять». КомбинацияmainBranchиdeployedOrReleasedдаёт двойную страховку: свежий контракт из главной ветки потребителя (чтобы разработка не разъезжалась) плюс те версии, которые прямо сейчас крутятся в продакшене (чтобы новый релиз поставщика не уронил уже задеплоенных клиентов). Если у потребителей длинные фича‑ветки, к списку добавляют{ matchingBranch: true }.Два параметра, которые в исходных гайдах обычно теряются, а в реальной жизни решают судьбу внедрения —
enablePendingиincludeWipPactsSince. Без них сценарий такой: потребитель добавил новое ожидание, опубликовал контракт, у поставщика немедленно упал main, хотя поставщик ещё физически не начал реализовывать этот эндпоинт. Виноватым оказывается contract testing, и практику тихо выпиливают.Pending pacts могут перевернуть порядок: непроверенный контракт приезжает к поставщику в статусе pending, верификация его прогоняет и показывает результат, но пайплайн не роняет. Поставщик реализует функциональность, верификация проходит, контракт автоматически перестаёт быть pending и с этого момента уже блокирует поломки. WIP‑контракты работают похоже для веток. Правильная последовательность внедрения выглядит так: сначала включаем pending, потом наблюдаем, потом ужесточаем.
Брокер и can‑i‑deploy
Контракты и результаты верификаций надо где‑то хранить, и обмениваться ими надо асинхронно. Этим занимается Pact Broker: открытый, поднимается Docker‑образом за вечер, требует Postgres. Pactflow (он же SmartBear Contract Testing) — коммерческая версия того же брокера с SSO, ролями, метриками и BDCT. Небольшой команде обычно хватает бесплатного тарифа, крупной организации приходится выбирать между платной подпиской и своим брокером в собственном контуре.
Главная ценность брокера даже не в хранении файлов, а в одной команде:
pact-broker can-i-deploy \ --pacticipant orders-ui \ --version $GIT_SHA \ --to-environment production \ --broker-base-url $PACT_BROKER_URL \ --broker-token $PACT_BROKER_TOKEN
Она отвечает на вопрос: безопасно ли катить вот эту сборку вот в это окружение? Брокер лезет в свою матрицу совместимости, смотрит, какие версии поставщиков сейчас живут в проде, и проверяет, прошла ли данная версия orders-ui верификацию против каждой из них.
Совместимость есть — код возврата 0, деплой едет дальше. Совместимости нет — релиз не уходит.
Симметрично это работает и для поставщика: перед его деплоем брокер проверяет, удовлетворит ли новая версия контракты всех потребителей, которые сейчас в проде.
Чтобы матрица не была пустой, брокеру надо рассказывать о фактах деплоя. Этим занимаются record-deployment и record-release. Без них can-i-deploy просто не знает, что где стоит, и честно отказывается отвечать. Пропущенный record-deployment — причина примерно половины вопросов «почему can‑i‑deploy ругается на unknown» в первый месяц эксплуатации.
Пайплайн целиком
На стороне потребителя порядок такой: прогнать тесты, опубликовать контракты с версией и веткой, спросить разрешение на деплой, задеплоить, зафиксировать факт деплоя.
- name: Run consumer tests run: npm test - name: Publish pacts run: | npx pact-broker publish ./pacts \ --consumer-app-version $GIT_SHA \ --branch $GIT_BRANCH \ --broker-base-url $PACT_BROKER_URL \ --broker-token $PACT_BROKER_TOKEN - name: Can I deploy to staging? run: | npx pact-broker can-i-deploy \ --pacticipant orders-ui \ --version $GIT_SHA \ --to-environment staging \ --retry-while-unknown 12 \ --retry-interval 10 \ --broker-base-url $PACT_BROKER_URL \ --broker-token $PACT_BROKER_TOKEN - name: Deploy run: ./deploy.sh staging - name: Record deployment run: | npx pact-broker record-deployment \ --pacticipant orders-ui \ --version $GIT_SHA \ --environment staging
Флаги --retry-while-unknown и --retry-interval нужны для гонки, которая обязательно случится: потребитель опубликовал новый контракт и хочет катиться, а поставщик ещё не успел его верифицировать. Вместо мгновенного отказа can-i-deploy подождёт результата пару минут.
Пайплайн поставщика зеркальный: верификация контрактов из брокера, публикация результата, can-i-deploy, деплой, record-deployment. Чтобы верификация запускалась не только по коммиту, но и по появлению нового контракта, в брокере настраивают вебхук: событие contract_content_changed дёргает сборку поставщика. Без вебхука обратная связь растягивается до следующего коммита в репозиторий поставщика, и вся идея быстрой проверки размывается.
Отдельно стоит упомянуть, что Pact давно не только про HTTP. Message‑контракты покрывают асинхронное взаимодействие: контракт фиксирует не запрос и ответ, а форму события, а тест дёргает вашу доменную функцию‑обработчик напрямую, минуя Kafka или SQS. Тестируется схема сообщения и логика её обработки, а не брокер сообщений.
Bi‑Directional и Drift
Классический Pact требует, чтобы обе стороны писали Pact‑тесты. Это самая дорогая часть внедрения, особенно если у потребителей уже есть моки на MSW, Nock или WireMock, а у поставщика есть живая OpenAPI‑спецификация.
Bi‑Directional Contract Testing (BDCT) от Pactflow разрывает эту связку. Потребитель публикует свой контракт (Pact‑файл, в том числе сгенерированный из существующих моков), поставщик публикует OpenAPI‑спеку. Брокер сам сравнивает два артефакта и выносит вердикт о совместимости. Верификатор поставщику не нужен, переписывать консюмерские тесты тоже не нужно, порог входа падает с квартала до нескольких дней. Как побочный эффект, в процесс легко включаются QA и SDET, которым не приходится лезть в код консюмерских тестов.
Слабое место у BDCT ровно одно, зато принципиальное: OpenAPI‑спека принимается на веру. Если она генерируется из кода, всё хорошо. Если её пишут руками, она расходится с реальностью примерно всегда: поле стало необязательным, сервис отдаёт 422 вместо 400, кто‑то поправил хотфикс и не тронул спеку. Брокер сравнивает контракт со спекой, обе стороны зелёные, а прод падает.
Именно эту дыру закрывает Drift, CLI‑инструмент Pactflow. Он берёт OpenAPI‑спеку, гоняет по реальному запущенному API настоящие запросы и проверяет, что ответы соответствуют описанию: обязательные поля, типы, коды. Тесты декларативные, лежат YAML‑файлами рядом с кодом, есть Lua‑хуки для подготовки состояния. В BDCT‑схеме Drift встаёт перед публикацией спеки в брокер: сначала поставщик доказывает сам себе, что его спека не врёт, и только потом отдаёт её на сравнение с контрактами потребителей. Заодно Drift занял нишу, освободившуюся после того, как в начале 2026-го окончательно умер Optic.
Когда всё это не нужно
Contract testing стоит денег и времени, и есть конфигурации, где он не окупается.
Два‑три сервиса в одной команде, деплоящиеся вместе, одним пайплайном — брокер, матрица совместимости и
can-i-deployтут решают проблему, которой нет. Поговорить друг с другом дешевле.Публичный API с неизвестными потребителями — consumer‑driven подход бессмысленен: вы не соберёте контракты со всех, кто вами пользуется. Тут работает обратная логика, спека как источник истины плюс проверка обратной совместимости при изменениях.
Внешние API, которые вы не контролируете. Написать contract‑тест на API платёжного провайдера можно, но верифицировать его некому: провайдер ваш контракт проигрывать не станет. Получится обычный мок с лишними церемониями.
Наконец, живой E2E‑набор, который всех устраивает и не флакует. Такое встречается редко, но встречается, и ломать работающее ради методологии смысла нет.
С чего начинать
Не с общего решения по организации. С одной пары consumer‑provider, где болит прямо сейчас: постоянные поломки на границе, деплои, застревающие из‑за нестабильных E2E, регулярные разборки между командами о том, кто чего сломал.
На этой паре пишутся Pact‑тесты для пяти‑семи самых критичных сценариев, поднимается брокер (Docker или бесплатный тариф), в один пайплайн встраивается can-i-deploy, и включается pending.
Через месяц у вас появляется статистика: сколько раз брокер остановил заведомо ломающий деплой, сколько регрессий поймали до прода, сколько времени сэкономили на разборах. Это и есть аргумент для расширения практики, причём аргумент с цифрами, а не с презентацией про best practices.
Про ускорение старта в 2026-м стоит сказать отдельно. У Pactflow есть MCP‑сервер и набор agent skills для Claude Code, Copilot, Cursor и Windsurf. Скилл даёт ассистенту знание конкретных паттернов Pact и, что важнее, умение сходить в ваш брокер и вытащить оттуда список уже существующих provider states, чтобы не плодить дубликаты вроде «order exists» и «an order exists» на одно и то же. Первый набор тестов действительно пишется заметно быстрее. Проектировать за вас границы сервисов и решать, что вообще должно быть в контракте, ассистент по‑прежнему не будет.
Contract testing закрывает лишь одну часть проблемы интеграций. Надёжность межсервисного взаимодействия зависит и от того, как устроены API‑проверки, пайплайны и сами запросы между сервисами. Разобрать эти смежные темы можно на бесплатных открытых уроках:
15 июля, 18:00 — «Настройка GitLab Runners. Хаки по настройке и оптимизации сборок проектов». Записаться
Разберём настройку и ускорение пайплайнов, в которые можно встроить проверку контрактов перед релизом.21 июля, 20:00 — «UI и API тестирование с Java и Playwright». Записаться
Знакомство с практическими подходами к автоматизации API‑проверками и построение устойчивого тестового контура.4 августа, 20:00 — «Секреты межсервисных запросов: как сделать приложение быстрым и надёжным». Записаться
Как проектировать взаимодействие между сервисами так, чтобы сбои на границах не превращались в проблемы на проде.
Уроки проводят преподаватели‑практики. Во время встречи можно задать вопросы по теме и разобрать интересующие моменты.
Больше бесплатных уроков июля смотрите в дайджесте.