Files
progcode/editorial/agent-rewrites/161.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
17 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 161,
"slug": "editorial-2023-07-mechanism-contract-tests",
"title": "Почему schema match не равен совместимости API",
"excerpt": "Ответ API может пройти схему и всё равно сломать действие на экране. Разбираем три уровня контракта: форму JSON, ожидание consumer и проверку provider.",
"contentHtml": "<p>Экран получает HTTP 200, обязательные ключи на месте, типы совпадают с OpenAPI. Но кнопка продления не появляется: ответ содержит <code>state: \"active\"</code> и <code>renewalAt: null</code>. Схема допускает оба значения. Consumer ожидал дату, по которой можно показать действие. Пользователь видит неполный сценарий, поддержка получает жалобу, а команда спорит, был ли релиз совместимым.</p>\n<p>Цена ошибки растёт из-за ложного зелёного сигнала. Проверка JSON подтверждает форму, но не подтверждает, что consumer сможет закончить свой сценарий. Provider считает, что поле не менялось. Consumer видит изменение смысла. Владелец релиза видит успешный job и не получает основания остановить выкладку.</p>\n<p>Тезис статьи простой: контракт API состоит как минимум из трёх разных доказательств. Schema match проверяет структуру. Consumer expectation проверяет нужное поведение. Provider verification проверяет, что конкретная версия provider действительно отвечает опубликованному interaction. Один результат нельзя выдавать за другой.</p>\n<h2>Три вопроса к одному ответу</h2>\n<p>Сначала отделите форму от смысла. OpenAPI описывает интерфейс, который могут использовать люди и инструменты. Schema Object задаёт типы, обязательность, перечисления и допустимые варианты. Это хороший барьер против пропавшего ключа, числа вместо строки и неизвестного значения enum.</p>\n<p>Но схема не знает, какое действие должен показать конкретный экран. Поле <code>renewalAt</code> может быть nullable для одного клиента и обязательным условием для другого сценария. Поэтому второй уровень должен принадлежать consumer: «для экрана продления активная подписка должна иметь применимую дату». Это уже не только свойство JSON. Это правило принятия решения.</p>\n<p>Третий уровень связывает ожидание с provider. Provider verification исполняет interaction на согласованном состоянии provider и сравнивает фактический ответ с контрактом. Без такого результата у команды есть описание ожидания, но нет доказательства, что provider его выполнил.</p>\n<table><caption>Что означает каждый результат</caption><thead><tr><th>Симптом</th><th>Причина</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>Ответ 200, экран не показывает действие</td><td>Смысл поля шире ожидания consumer</td><td>Воспроизвести decision rule на ответе <code>active + null</code></td><td>Уточнить state, добавить явное поле или изменить consumer</td></tr><tr><td>Пропал ключ или изменился тип</td><td>Нарушена schema-граница</td><td>Проверить required, type, enum и nullable для версии схемы</td><td>Исправить provider либо согласовать версионное изменение</td></tr><tr><td>Consumer contract зелёный, provider не проверен</td><td>Проверили только mock-ответ</td><td>Найти результат verification для версии provider и provider state</td><td>Не называть выпуск совместимым до реального результата</td></tr><tr><td>Один interaction зелёный, старый клиент сломан</td><td>Contract покрывает не всех consumers</td><td>Сверить список клиентов и поддерживаемые версии</td><td>Добавить interaction или ограничить решение областью проверки</td></tr></tbody></table>\n<h2>Учебный пример: active не обещает дату</h2>\n<p>Рассмотрим искусственный сценарий <code>GET /v1/subscriptions/sub-42</code>. Имена <code>synthetic-portal-web</code> и <code>synthetic-billing-api</code> нужны только для объяснения механизма. Это не лог реального сервиса, не результат запуска и не утверждение о production.</p>\n<p>Общая schema может разрешать такой ответ:</p>\n<pre><code>{\n \"id\": \"sub-42\",\n \"state\": \"active\",\n \"renewalAt\": null\n}</code></pre>\n<p>На уровне формы ответ выглядит допустимым. На уровне consumer он не подходит экрану продления: у экрана нет даты и он не должен выдумывать её. Правило можно записать рядом с interaction:</p>\n<pre><code>const expectation = ({ state, renewalAt }) =&gt;\n state === 'active' &amp;&amp; isFutureIsoDate(renewalAt);\n\nexpect(expectation(response)).toBe(true);</code></pre>\n<p>Этот код показывает только идею decision rule. Он не валидирует OpenAPI-документ, не вызывает HTTP, не поднимает provider и не запускает Pact. В реальном тесте надо определить формат даты, часовой пояс, момент отсчёта и provider state. Если эти условия не названы, тест может пройти на случайных данных и не защищать нужный сценарий.</p>\n<p>Теперь различие видно на трёх ответах. <code>active</code> с будущей датой может пройти форму и ожидание. <code>active + null</code> может пройти форму, но нарушить ожидание consumer. Число в <code>state</code> должно остановиться уже на схеме. Такая классификация полезнее единого флага <code>compatible: true</code>: она показывает, где именно возникло расхождение.</p>\n<figure><img src=\"/assets/editorial/2023/contract-tests-2023-compatibility-matrix.svg\" alt=\"Матрица трёх уровней проверки контракта API: schema, consumer expectation и provider verification\"><figcaption>Матрица разделяет форму ответа, смысл для consumer и фактическую проверку provider. Отметки относятся к учебной модели и не являются результатом production-запуска.</figcaption></figure>\n<h2>Как записать contract, который помогает принять решение</h2>\n<p>Начните с одного действия consumer, а не со всего API. Назовите метод, путь, вход, ожидаемый статус и минимальный ответ. Затем запишите provider state. Формулировка «подписка активна» слишком общая, если экрану нужна именно дата продления после текущего момента. State должен объяснять, почему provider обязан вернуть нужные данные.</p>\n<p>Отдельно укажите отрицательный путь. Например: если <code>state</code> равен <code>active</code>, но <code>renewalAt</code> отсутствует или уже прошёл, consumer не показывает кнопку продления и сообщает, что действие недоступно. Это не означает, что поле надо сделать non-null для всех клиентов. Отрицательная ветка фиксирует решение одного сценария.</p>\n<p>Храните рядом версии. Укажите версию schema, consumer contract, provider и provider state. Версия OpenAPI не заменяет версию API-сборки. Версия consumer не доказывает, какую сборку provider проверяли. Эти указатели нужны, чтобы зелёный результат можно было воспроизвести и связать с конкретным изменением.</p>\n<h2>Порядок проверки</h2>\n<ol><li><strong>Зафиксируйте симптом.</strong> Запишите request, response, статус и пользовательское действие, которое не завершилось. Не называйте причину до проверки.</li><li><strong>Проверьте форму.</strong> Сверьте версию schema, media type, required-поля, типы, enum и nullable-границы. Если форма нарушена, исправляйте её прежде, чем обсуждать смысл.</li><li><strong>Опишите решение consumer.</strong> Укажите, какое поле запускает действие и что должен сделать клиент при его отсутствии, просрочке или неизвестном значении.</li><li><strong>Назовите provider state.</strong> Определите состояние данных, в котором interaction должен быть выполнен. Не заменяйте его удобным mock-ответом без объяснения.</li><li><strong>Запустите provider verification.</strong> Исполните опубликованный contract против согласованной версии provider. Сохраните результат, версию и список проверенных interactions.</li><li><strong>Примите ограниченное решение.</strong> Разрешайте выпуск только для проверенных consumers, states и версий. Для остальных оставьте явный риск или остановите изменение.</li></ol>\n<h2>Почему schema match недостаточен</h2>\n<p>Schema проверяет допустимость значения, а не его полезность для каждого клиента. Nullable-поле может быть корректным по общему договору и непригодным для конкретного действия. Enum может сохранить прежний набор строк, но поменять бизнес-смысл каждой строки. HTTP 200 может сообщать об успешной обработке запроса, но не о готовности пользовательского шага.</p>\n<p>Consumer-driven contract помогает сузить проверку до реальной потребности клиента. Он не пытается описать все возможные ответы provider. Это достоинство для быстрого feedback, но и ограничение: неохваченный consumer остаётся неохваченным. Список interactions надо поддерживать вместе со списком клиентов, иначе команда легко перенесёт результат одного экрана на весь API.</p>\n<p>Provider verification тоже не даёт универсальной гарантии. Она подтверждает конкретные interactions в подготовленных состояниях. Она не заменяет авторизацию, миграцию данных, нагрузочные проверки, совместимость старых мобильных версий и наблюдение после выкладки. Эти проверки отвечают на другие вопросы.</p>\n<h2>Ограничения и отрицательный путь</h2>\n<p>Учебный код выше не доказывает совместимость реальных версий. В нём нет сети, broker, Pact, авторизации, зависимостей provider и production-данных. Даже корректный локальный результат означает только то, что правило примера отделяет форму от смысла. Нельзя писать в release-описании «provider verified», если запуск provider verification не состоялся.</p>\n<p>Если verification не прошла, сначала сохраните исходный contract и ответ. Затем решите, где находится граница изменения. Иногда provider должен вернуть прежний смысл. Иногда consumer должен перестать трактовать <code>active</code> слишком узко. Иногда безопаснее добавить новое поле и временно поддержать оба варианта. Автоматически делать nullable-поле обязательным нельзя: это может сломать другие сценарии.</p>\n<p>Rollback также требует конкретики. Назовите версии, которые можно вернуть, данные, уже записанные новым кодом, и consumer, который ещё читает старый ответ. Snapshot JSON не откатывает endpoint, базу, флаг или опубликованный артефакт. Если эти условия неизвестны, готовность к rollback не доказана.</p>\n<h2>Критерий готовности</h2>\n<p>Изменение готово к выпуску, когда выполнены все четыре условия: schema проверена для нужной версии; consumer expectation содержит положительную и отрицательную ветки; provider state и версия provider названы; provider verification дала сохранённый результат для каждого consumer, которого затрагивает изменение. Если хотя бы одного пункта нет, вывод должен звучать точнее: «форма проверена», «ожидание записано» или «verification не запускалась». Слово «совместимо» оставляйте только для доказанной области.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://spec.openapis.org/oas/v3.1.0.html\" target=\"_blank\" rel=\"noopener noreferrer\">OpenAPI Specification 3.1.0</a> — назначение спецификации и границы Schema Object.</li><li><a href=\"https://docs.pact.io/implementation_guides/go/docs/provider\" target=\"_blank\" rel=\"noopener noreferrer\">Pact: Provider Verification</a> — последовательность публикации contract и воспроизведения interactions на provider.</li><li><a href=\"https://docs.pact.io/getting_started/verifying_pacts\" target=\"_blank\" rel=\"noopener noreferrer\">Pact: Verifying Pacts</a> — почему одного contract недостаточно без результата verification.</li></ul>"
}