{"index":160,"slug":"editorial-2023-07-field-contract-tests","title":"Контракт API прошёл schema-проверку, но сломал сценарий: как найти расхождение смысла","excerpt":"Валидный JSON не доказывает совместимость consumer и provider. Разбираем учебный случай с nullable-полем, отделяем форму ответа от его смысла и задаём проверяемый шлюз перед выпуском.","contentHtml":"

После выкладки экран может получить ответ со статусом 200, пройти проверку схемы и всё равно не показать пользователю нужное действие. Например, provider возвращает state: "active" и renewalAt: null. OpenAPI допускает такое тело. Consumer видит active, но строит экран продления по дате и оставляет кнопку недоступной.

Симптом наблюдаем: запрос успешен, JSON корректен, а пользовательский сценарий остановился. Цена ошибки — сломанный экран и неверное решение о релизе. Команда может откатить полезное изменение без доказательства или оставить несовместимость до следующей выкладки.

Тезис: schema проверяет форму, а contract interaction проверяет конкретное использование API. Решение о выпуске должно связывать consumer, provider, версии, состояние provider и результат verification. Один зелёный schema-check не доказывает совместимость всех клиентов.

Форма и смысл ответа

Schema описывает типы, обязательность, enum, media type и допустимость null. Она ловит удалённое поле, неверный тип и неожиданный статус. Но она не знает, какую кнопку должен показать конкретный consumer.

Consumer contract фиксирует более узкий вопрос: какой запрос отправляет клиент и какой ответ нужен его сценарию. Экран продления может принимать только active вместе с будущей датой. Другой consumer может законно использовать active без даты: ему достаточно показать состояние подписки. Поэтому правило renewalAt нельзя молча объявить глобальным правилом API.

Provider verification проверяет interaction на стороне provider. Для неё нужно назвать provider state: подписка активна, продление разрешено, дата существует или дата отсутствует. Фраза «вернулся active» недостаточна. Иначе тест закрепляет удобный ответ, а не сценарий пользователя.

Симптом → причина → проверка → действие
СимптомПричинаПроверкаДействие
200, schema PASS, действие недоступноConsumer ждёт смысл, которого ответ не обещаетСопоставить scenario, поле и ветку интерфейсаУточнить expectation или добавить явное поле
Допустимый request получает 400Разошлись enum, required или provider stateСверить interaction, schema и состояние данныхИсправить контракт или совместимость provider
Verification не запускаетсяНет версии contract/provider или результатаНайти точный artifact и PASS/FAILОстановить шлюз до появления evidence
Старый consumer падает после нового enumКлиент не знает новое значениеПроверить все поддерживаемые версииСохранить обратную ветку или добавить поле

Учебный пример: active без даты

Это synthetic response, а не production-данные и не отчёт об инциденте. Он показывает границу между формой и смыслом.

const response = { status: 200, body: { state: "active", renewalAt: null } }; const schemaResult = response.status === 200 && response.body.state === "active"; const semanticResult = response.body.state !== "active" || response.body.renewalAt !== null; console.log({ schemaResult, semanticResult }); // { schemaResult: true, semanticResult: false }

Учебная проверка специально строже для одного сценария. Она не говорит, что null запрещён в API. Она говорит только: экрану продления нужна дата. В реальном проекте это правило подтверждает владелец сценария и фиксирует рядом с consumer contract.

Отрицательный путь важнее зелёного. Если дата отсутствует, consumer не должен подставлять текущий день, показывать фиктивную дату или бесконечно повторять запрос. Он должен скрыть действие, объяснить недоступность или выбрать безопасную ветку. Это часть контракта, которую одна JSON Schema не описывает.

Как собрать доказательство

Сначала сохраните исходный contract до изменения provider. Запишите consumer, provider, scenario, request, ожидаемый response, версию contract и версию provider. Не переписывайте contract под новый ответ: иначе пропадёт точка сравнения.

Отдельно проверьте форму: статус, Content-Type, required, enum, типы и null. Если форма не совпала, это самостоятельная причина отказа. Если совпала, проверьте semantic expectation: какое действие принимает consumer и какое условие ему нужно.

Затем provider выполняет interaction в контролируемом provider state. Результат связывается с точной версией provider. Ответ из лога не заменяет verification: он может относиться к другой сборке, данным или consumer. Если используется Pact Broker, результат должен попасть в матрицу, по которой релиз принимает решение.

\"Шлюз
Interaction, provider state и версия provider должны привести к результату verification. Отсутствующий результат не превращается в зелёное разрешение.

Не смешивайте статусы. Schema PASS означает совпадение с формой. Semantic FAIL означает, что конкретный consumer не может продолжить сценарий. Provider verification not-run означает, что совместимость с исполняемой версией ещё не доказана.

Порядок действий

  1. Зафиксируйте симптом. Сохраните один request, response, пользовательский эффект, время и доступные версии. Не называйте виновника заранее.
  2. Опишите expectation. Назовите поле и состояние, нужные consumer. Добавьте ветку для null, неизвестного enum и отсутствующей даты.
  3. Проверьте форму. Сопоставьте schema, статус, media type, required, enum и типы. Проверьте допустимость null в нужной версии схемы.
  4. Проверьте сценарий. Запустите interaction с названным provider state и точной версией provider. Не подменяйте слой, который должен формировать ответ.
  5. Выберите обратимое действие. При неизвестном результате остановите выпуск, сохраните старое поведение или включите согласованный fallback. Не объявляйте совместимость по schema PASS.
  6. Закройте цепочку. Опубликуйте результат с версиями и окружением. Перед deploy проверьте матрицу совместимости, после deploy запишите фактическую версию и среду.

Откат и отрицательный путь

Откат оправдан, если новый provider уже влияет на поддерживаемый consumer, а совместимого поведения нет. Сначала определите границу: версия provider обратима, а данные, созданные новым consumer, могут быть необратимы. Проверьте миграции, записи и feature flags отдельно.

Semantic FAIL не всегда означает дефект provider. Возможно, provider всегда считал active широким состоянием, а consumer ошибочно использовал его как гарантию даты. Тогда исправление нужно в consumer. Возможны также явное поле canRenew, временная поддержка двух форм или обновление старого consumer до изменения provider.

Если неизвестны consumer, provider state, версия или verification result, шлюз не должен трактовать неизвестность как PASS. Выпуск останавливается с объяснимой причиной. Молчаливое разрешение создаёт ложную уверенность.

Ограничения

Consumer-driven contract покрывает зафиксированные interactions, а не все ответы provider. Он не заменяет интеграционные, компонентные и end-to-end проверки. Он также не определяет бизнес-смысл сам: ошибочное expectation может надёжно защищать неправильное решение.

Contract test не проверяет автоматически auth, feature flags, миграции, лимиты, retries и downstream-зависимости. Их включают в provider state или проверяют отдельно, если они меняют ответ. Учебный пример из статьи не запускает сеть, broker, CI или provider. Его значения нельзя выдавать за измерение совместимости или за основание production rollback.

OpenAPI описывает интерфейс, но не знает, какую кнопку показать consumer. Pact связывает consumer expectations с provider verification, но требует дисциплины версий и окружений. Verification без точной версии provider или с неверным состоянием данных создаёт видимость доказательства.

Проверяемый критерий готовности

Изменение готово к выпуску, когда для каждого затронутого consumer есть versioned contract и scenario; schema-check прошёл; отрицательная ветка описана; provider verification выполнилась в нужном provider state; PASS связан с точной версией provider; решение о deploy проверено по актуальной матрице. Если любой пункт неизвестен, статус — «не готово к выпуску».

В учебном случае ответ 200 с active и renewalAt: null проходит формальную схему, но не проходит ожидание экрана продления. Это не доказывает дефект provider. Это требует уточнить семантику и выполнить настоящую provider verification до решения о выпуске.

Проверяемые источники

"}