{"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 | Клиент не знает новое значение | Проверить все поддерживаемые версии | Сохранить обратную ветку или добавить поле |
Это 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, результат должен попасть в матрицу, по которой релиз принимает решение.
Не смешивайте статусы. Schema PASS означает совпадение с формой. Semantic FAIL означает, что конкретный consumer не может продолжить сценарий. Provider verification not-run означает, что совместимость с исполняемой версией ещё не доказана.
null, неизвестного enum и отсутствующей даты.null в нужной версии схемы.Откат оправдан, если новый 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 до решения о выпуске.