Провайдер может поменять смысл уже существующего поля и не сломать форму ответа. В учебном примере GET /v1/subscriptions/sub-42 по-прежнему возвращает 200, строковый id, допустимый state: "active" и renewalAt: null. JSON проходит описанную схему, но экран продления у consumer рассчитывал получить точную дату. Ошибка проявляется после релиза не потому, что пропал ключ, а потому, что старое слово active стало обозначать более широкое состояние.
Цена такого расхождения обычно выше одной пустой кнопки. Consumer начинает объяснять пользователю чужое состояние, поддержка получает неповторяемый сценарий, а provider получает срочный откат без ясного критерия. Если команда смотрит только на HTTP-статус и типы, она может решить, что выпуск совместим, хотя нужное действие уже невозможно. Поэтому контракт полезно начинать не с полного описания API, а с одного поведения, за которое отвечает конкретный consumer.
Один сценарий, а не обещание за весь API
Здесь сценарий узкий: экран продления запрашивает одну подписку и должен показать действие только тогда, когда ответ содержит применимую дату. Consumer называется synthetic-portal-web, provider — synthetic-billing-api; это учебные имена, не наблюдение из чужой системы. Request и response не отправляются никуда. Они нужны, чтобы отделить три разных вопроса: совпадает ли форма, выполняется ли ожидание consumer и есть ли настоящий результат provider verification.
Первый вопрос относится к schema. В нём можно проверить, что id — непустая строка, state входит в перечисление, а renewalAt имеет календарно валидную ISO-дату или null. Второй вопрос относится к смыслу конкретного сценария: для экрана продления active без даты или с уже прошедшей датой не даёт пользователю выполнимого действия. Третий вопрос относится к процессу: настоящий provider verification выполняет опубликованный contract против согласованной версии provider и сохраняет результат. Эти вопросы не являются разными названиями одного PASS.
| Уровень | Что проверяется | Ответ active + null | Чего результат не доказывает |
|---|---|---|---|
| Schema match | поля, типы, enum и nullable-граница | может быть PASS | что consumer способен завершить свой сценарий |
| Semantic expectation | обещание «для экрана продления есть применимая дата» | FAIL | что provider реально запускался |
| Provider verification | исполнение interaction на согласованной версии provider в нужном state | возможен только после реального запуска | все сценарии, не вошедшие в contract |
| Release decision | связка contract version, provider result и версии consumer | не принимается по одной схеме | качество UX, нагрузку и миграции данных |
Запишите минимальный contract как проверяемое намерение
В OpenAPI полезно описать границы интерфейса: путь, метод, статус, поля и допустимый null. Версионный OAS 3.1.0 называет себя описанием HTTP-интерфейса и отдельно говорит, что часть семантики Schema Object определяется приложением. Значит, схема нужна, но не обязана выразить правило конкретного экрана. Если пытаться спрятать всё значение поля в один nullable-тип, reviewer увидит форму, но не увидит потребность consumer.
Consumer-driven contract добавляет другой артефакт: пример interaction, который нужен реальному коду consumer. В документации Pact такой contract возникает при исполнении consumer test и описывает конкретную пару request/response. Для нашего сценария важно не слово Pact, а дисциплина: назвать provider state, на котором ожидается дата, и назвать точную версию артефакта. Без provider state пример остаётся красивым JSON, который provider может воспроизвести только случайно.
Исполнимый synthetic fixture: увидеть границу, не подменить запуск
Ниже не находится библиотека Pact и не запускается HTTP. Функция принимает только объекты с явными маркерами synthetic-contract-input-v1 и synthetic-provider-response-v1. Она специально возвращает actualProviderVerification: "not-executed" даже для хорошего учебного ответа. Благодаря этому пример можно выполнить локально и одновременно нельзя выдать его за результат consumer/provider проверки.
import {
createSyntheticContract,
inspectSyntheticProviderResponse,
} from './web/scripts/upgrade-2023-07.mjs';
const contract = createSyntheticContract({
marker: 'synthetic-contract-input-v1',
consumer: 'synthetic-portal-web',
provider: 'synthetic-billing-api',
operation: 'GET /v1/subscriptions/{id}',
scenario: 'renewal-screen-needs-an-actionable-date',
});
const changedMeaning = inspectSyntheticProviderResponse(contract, {
marker: 'synthetic-provider-response-v1',
value: { id: 'sub-42', state: 'active', renewalAt: null },
});
console.log(changedMeaning.schemaMatch); // true
console.log(changedMeaning.semanticExpectationMatch); // false
console.log(changedMeaning.actualProviderVerification); // not-executed
Здесь active + null даёт schemaMatch: true: типы и nullable-правило соблюдены. Но semanticExpectationMatch: false, потому что выбранный экран не может показать дату действия. Fixture также отклоняет для смысла дату раньше фиксированного synthetic reference time 2023-07-10T00:00:00Z, а несуществующую календарную дату уже не пропускает schema. Это не доказывает дефект в чьём-либо API. Это лишь показывает, что правило consumer не было покрыто структурной проверкой. Когда такой результат найден в реальном change, его надо превратить в вопрос к владельцам поведения, а не в автоматическое требование сделать поле non-null везде.
Маршрут: симптом → причина → проверка → действие
- Симптом. Запишите видимый эффект без диагноза:
200и валидная форма ответа есть, но экран не может показать обещанное действие. - Причина. Найдите поле, чьё бытовое имя скрывает правило. В примере это не «неверный JSON», а незафиксированное значение
activeдля одного consumer-сценария. - Проверка формы. Сверьте путь, статус, обязательные поля, enum и nullable-границу с версией schema. Отметьте отдельно, какие значения форма сознательно допускает.
- Проверка смысла. Добавьте один пример, где consumer реально принимает решение. Назовите provider state и минимальный ответ, без которого сценарий должен отказать или показать иной путь.
- Действие. Передайте contract вместе с версией consumer и ожидаемым provider state. Настоящий verifier должен исполнить его отдельно; до результата нельзя писать «совместимо».
- Решение выпуска. Привяжите результат к конкретным версиям и известным consumer. Не переносите PASS одной interaction на невписанные методы и редкие состояния.
Что именно требуется от настоящей provider verification
Реальная provider verification — это не сериализация JSON в CI-логе. Нужны как минимум contract, версия provider, управляемый provider state, фактическое исполнение interaction и результат, который можно сопоставить с версией. В документации Pact отдельно советуют проверять provider локально или в CI, а не против уже развернутого сервиса: так можно контролировать состояния и зависимости. Это процессная рекомендация, не обещание, что любой набор mock данных воспроизводит production.
Успешный provider verification отвечает на узкий вопрос: данный provider в подготовленном состоянии удовлетворил данному набору interactions. Он сильнее, чем schema match, потому что связывает ожидание с поведением. Но он всё ещё не заменяет тест самого consumer, проверку авторизации, миграцию данных, наблюдение после выпуска или анализ всех возможных клиентов. Граница важна: чем точнее названа, тем меньше ложного спокойствия после зелёного job.
Ограничение, rollback и следующий шаг
Fixture этого пакета создаёт только замороженные объекты в памяти. Он не вызывает Pact, broker, HTTP-клиент, provider, сеть, CI, schema registry или production. Его PASS не сообщает о совместимости реальных версий и не утверждает, что state где-либо действительно менялся. Даже rollback в fixture восстанавливает только snapshot учебного contract, а не endpoint, данные, флаг или опубликованный артефакт.
Для настоящего изменения rollback стоит спланировать до выкладки: какие версии consumer ещё читают старое значение, можно ли временно оставить старый смысл или добавить новое явное поле, кто отменяет публикацию contract и где будет виден результат. Следующий малый шаг — взять один реальный consumer-сценарий, записать его без предположений о production и назначить владельца provider verification. Если provider state или версия не названы, выпуск ещё не имеет достаточного условия готовности.
Историческая граница июля 2023
К июлю 2023 уже были доступны OAS 3.1.0 от 15.02.2021 и приведённые страницы документации Pact, обновлённые в августе 2022. Они помогают различить описание интерфейса, contract by example и provider verification. Ни один источник не сообщает ничего о synthetic сервисах этого примера и не превращает их в доказательство production-совместимости.
Проверяемые источники
- OpenAPI Specification v3.1.0, 15 февраля 2021 — версионная первичная спецификация, доступная до июля 2023. OAS описывает интерфейс HTTP API и типы; для части свойств она прямо оставляет прикладную семантику потребляющему приложению.
- Pact Docs: Introduction to contract testing, обновлено 30 августа 2022 — официальная документация Pact, исторически доступная до июля 2023. Она различает статическую schema/specification и contract by example, который формируется выполнением consumer tests.
- Pact Docs: Verifying Pacts, обновлено 11 августа 2022 — официальная документация Pact, исторически доступная до июля 2023. В ней provider verification привязан к локальному provider или CI, состояниям provider и опубликованному результату; этот sidecar такого запуска не выполняет.