DarkRiDDeR12 мин

Контракт API до релиза: зафиксировать смысл ответа, а не только JSON

ТестированиеAPI

Провайдер может поменять смысл уже существующего поля и не сломать форму ответа. В учебном примере 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 везде.

Три роли учебного contract flow: consumer формулирует сценарий экрана продления, артефакт фиксирует request, provider state и semantic expectation, а provider verification требует отдельного исполненного результата для точных версий.
Схема показывает роли и границы доказательства. Стрелки не являются сетевой трассой, а блок provider verification не означает, что он выполнялся для этого sidecar.

Маршрут: симптом → причина → проверка → действие

  1. Симптом. Запишите видимый эффект без диагноза: 200 и валидная форма ответа есть, но экран не может показать обещанное действие.
  2. Причина. Найдите поле, чьё бытовое имя скрывает правило. В примере это не «неверный JSON», а незафиксированное значение active для одного consumer-сценария.
  3. Проверка формы. Сверьте путь, статус, обязательные поля, enum и nullable-границу с версией schema. Отметьте отдельно, какие значения форма сознательно допускает.
  4. Проверка смысла. Добавьте один пример, где consumer реально принимает решение. Назовите provider state и минимальный ответ, без которого сценарий должен отказать или показать иной путь.
  5. Действие. Передайте contract вместе с версией consumer и ожидаемым provider state. Настоящий verifier должен исполнить его отдельно; до результата нельзя писать «совместимо».
  6. Решение выпуска. Привяжите результат к конкретным версиям и известным 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 такого запуска не выполняет.