Как QA оценивает риск для старых потребителей и выбирает самую раннюю проверку
Backend меняет ответ GET /orders. Новая OpenAPI-спецификация валидна, модульные тесты проходят, но интеграционное окружение появится только перед релизом. В этот момент тестировщику недостаточно проверить, что новый ответ соответствует новой документации. Нужно понять, продолжат ли с ним работать уже выпущенные клиенты.
Часть таких проблем можно обнаружить ещё по изменению контракта. Другие становятся видны только при проверке реализации или конкретного потребителя. Есть и третий класс, структура запроса и ответа не меняется, но меняется смысл данных. Универсальной проверки для всех трёх случаев нет, поэтому задача QA состоит не в том, чтобы выбрать один инструмент, а в том, чтобы определить риск и найти самый ранний уровень, на котором его можно проверить.
Дальше под интеграционными тестами будем понимать совместный прогон реального потребителя и провайдера на общем окружении. Проверки спецификации, изолированный запуск провайдера и consumer-driven contracts можно выполнить раньше, ещё в pull request.
Сначала понять, что именно изменилось
Возьмём API, который возвращает список заказов. Старый клиент отправляет запрос без обязательных фильтров, показывает сумму, переводит статус в подпись и рассчитывает, что первым придёт самый свежий заказ.
GET /orders?limit=20 { "items": [ { "id": "ord-142", "status": "PAID", "totalMinor": 259900, "currency": "RUB", "createdAt": "2026-08-15T10:30:00Z" } ] }
В контракте поля id, status, totalMinor, currency и createdAt обязательны. totalMinor имеет тип integer и содержит сумму в минимальных денежных единицах, для рублей это копейки. Допустимые статусы ограничены значениями NEW и PAID.
Первый вопрос при ревью изменений: API стало строже к тому, что получает, или свободнее в том, что возвращает? Для запроса и ответа безопасное направление противоположно.
Где изменили контракт |
Обычно совместимое направление |
Что создаёт риск для старого клиента |
|---|---|---|
Запрос |
Новый сервер принимает больше вариантов входных данных |
Сервер перестаёт принимать запрос, который был допустим раньше |
Ответ |
Новый сервер сохраняет прежние обязательства и не удаляет ожидаемые данные |
Ответ допускает форму или значение, к которым старый клиент не готов |
Например, дополнительное необязательное поле запроса обычно не мешает старому клиенту: он продолжает отправлять прежний запрос. Если это поле стало обязательным, старый запрос может получить 400. В ответе добавление обычного поля чаще всего совместимо, но удаление обязательного поля или появление null там, где раньше всегда было значение, уже меняет ожидания клиента.
Из этого правила есть практические исключения. Некоторые клиенты используют строгую десериализацию и отклоняют неизвестные поля, хотя спецификация допускает расширение ответа. Поэтому статическая совместимость контракта и фактическая совместимость с конкретным клиентом - не одно и то же.
Что QA узнает из сравнения OpenAPI
Проверка одной спецификации отвечает только на вопрос, корректно ли она записана. Чтобы искать потенциально несовместимые изменения, нужна предыдущая версия контракта. Причём сравнивать кандидат следует не просто с main, а с последним опубликованным контрактом, который используют поддерживаемые клиенты.
oasdiff breaking openapi-released.yaml openapi-candidate.yaml --fail-on WARN
Предположим, в новой версии сделали четыре изменения: параметр region стал обязательным, totalMinor удалили из ответа, его тип поменяли с integer на string в другом методе, а в status добавили значение REFUNDED.
Первые три изменения можно связать с конкретным нарушением старого контракта. Запрос без region больше невалиден, обязательное поле исчезает, а значение другого типа может не пройти десериализацию. Добавление значения в response enum сложнее: сервер расширяет множество возможных ответов, и клиент с исчерпывающим switch без fallback может упасть. Поэтому oasdiff по умолчанию сообщает о таком изменении как о предупреждении, а не как об однозначной ошибке.
Для QA отчёт diff-инструмента не должен быть финальным вердиктом. Каждый сигнал нужно перевести в наблюдаемую поломку старого потребителя. Если region стал обязательным, полезная проверка - отправить запрос в старой форме и посмотреть, сохраняется ли прежнее поведение. Если расширился enum, нужно выяснить, как старый клиент обрабатывает неизвестное значение, а не ограничиваться тем, что YAML изменился.
Зелёный diff тоже имеет узкое значение: выбранный инструмент не нашёл несовместимости между двумя переданными ему спецификациями по включённым правилам. Он ничего не говорит о фактической реализации, неизвестных клиентах и правилах, которые существуют только в требованиях или в голове команды.
Когда нужен контракт конкретного потребителя
OpenAPI описывает поверхность API целиком. Consumer-driven contract, или контракт, управляемый потребителем, фиксирует только те запросы и свойства ответа, на которые рассчитывает конкретный клиент. Потребитель формирует свои ожидания, а провайдер проверяет кандидатную реализацию против них в изоляции.
Для нашего клиента контракт может содержать такой набор ожиданий:
GET /orders?limit=20 -> 200 items[*].id : string, required items[*].status : NEW | PAID, required items[*].totalMinor : integer, required items[*].currency : string, required
Удаление totalMinor или смена его типа сломает такую проверку. Новый обязательный region проявится, если реализация провайдера начнёт отклонять старый запрос, воспроизведённый из контракта. При этом удаление поля, которым этот клиент не пользуется, может не повлиять на его контракт, хотя для API в целом изменение остаётся несовместимым.
Расширение enum тоже не гарантированно будет обнаружено. Provider verification обычно воспроизводит конечный набор взаимодействий. Если подготовленное состояние возвращает PAID, проверка ничего не узнает о новом REFUNDED. Чтобы оценить риск, QA должен либо проверить fallback в клиенте, либо добавить состояние, в котором провайдер действительно возвращает неизвестное для старой версии значение.
Consumer contracts особенно полезны, когда сервис имеет несколько известных потребителей с независимыми релизными циклами. Но они не могут доказать безопасность публичного API, если часть клиентов неизвестна и их ожидания нигде не опубликованы. В такой системе правила общей обратной совместимости должны оставаться строже ожиданий отдельных известных потребителей.
Зелёный контракт не защищает от изменения смысла
Теперь изменим API так, чтобы структура осталась прежней. totalMinor по-прежнему integer, но backend начинает передавать в поле рубли, а не копейки. Вместо 259900 он возвращает 2599. OpenAPI diff не видит проблемы: имя, тип и обязательность поля не поменялись. Контракт потребителя с проверкой типа тоже остаётся зелёным. Старый клиент делит значение на 100 и показывает 25,99 рубля вместо 2599.
Такое изменение относится не к структурной, а к семантической совместимости. Поймать его можно раньше интеграционного стенда, но для этого нужна другая проверка: компонентный тест провайдера с бизнес-инвариантом. Например, для заказа с известной ценой и количеством тест должен проверить точное значение totalMinor, а не только тип integer.
Похожая ситуация возникает с сортировкой. Если backend меняет порядок по умолчанию с createdAt DESC на ASC, схема ответа остаётся прежней. Контракт с одним заказом также проходит. Самая ранняя содержательная проверка здесь - компонентный тест с несколькими заказами и разными датами, который явно проверяет порядок элементов.
К этому же классу относятся правила округления, значения по умолчанию, трактовка пустого результата, права доступа и побочные эффекты операции. Часть из них можно выразить в OpenAPI ограничениями, но произвольный бизнес-смысл из схемы автоматически не выводится. Текст в description делает требование видимым для человека, однако не превращает его в исполняемую проверку.
Как выбрать самую раннюю проверку
Выбор начинается не с инструмента, а с утверждения, которое нужно доказать. У каждого уровня есть своя граница.
Риск |
Самая ранняя полезная проверка |
Что она подтверждает |
Изменилась объявленная форма запроса или ответа |
OpenAPI diff |
Старый и новый машиночитаемые контракты совместимы по выбранным правилам |
Реализация разошлась со спецификацией |
Проверка провайдера по OpenAPI |
Фактические ответы и статусы соответствуют опубликованному контракту в проверенных сценариях |
Может сломаться известный клиент |
Consumer contract или тест API-клиента |
Зафиксированные ожидания конкретного потребителя выполняются |
Типы прежние, но изменилось бизнес-поведение |
Компонентный функциональный тест |
Сохраняется нужная семантика: единицы, порядок, значения по умолчанию, инварианты |
Риск зависит от реальной связи сервисов и окружения |
Интеграционный тест |
Работают авторизация, конфигурация, сеть, данные и совместный сценарий |
На практике QA может пройти по изменению контракта в следующем порядке:
Определить baseline: какая версия API уже опубликована и какие старые клиенты ещё поддерживаются.
Отделить изменения запроса от изменений ответа и проверить безопасное направление совместимости.
Сравнить спецификации и для каждого сигнала сформулировать, как именно может сломаться старый потребитель.
Посмотреть на реальные ожидания клиента: обязательные поля, обработку enum и null, строгую десериализацию, значения по умолчанию.
Найти правила, которые не выражены структурой: единицы измерения, сортировку, округление, состояние данных и побочные эффекты.
Выбрать самый ранний тест, который способен проверить именно это правило, и отдельно зафиксировать, что остаётся на интеграционный прогон.
Такой маршрут не требует переносить всё тестирование в контрактный слой. Его задача скромнее. Не нужно ждать общего стенда там, где несовместимость уже видна из двух YAML-файлов, клиентского контракта или изолированного поведения провайдера.
Что в итоге можно поймать до интеграционных тестов
Значительную часть breaking changes можно обнаружить ещё в pull request, но только если не называть одним словом разные классы проблем. Статическое сравнение показывает несовместимость объявленных контрактов. Consumer contract проверяет ожидания известного клиента. Компонентные тесты защищают формализованную семантику. Интеграционному уровню остаются риски, которые действительно возникают только при совместной работе систем.
Поэтому зелёный OpenAPI diff не означает, что старый клиент точно работает. Зелёный consumer contract не означает, что проверено всё API. Даже зелёный интеграционный сценарий подтверждает только покрытый маршрут. Полезный результат для QA - не общий статус green, а точное понимание, какую совместимость проверили, на каком уровне и какие предположения всё ещё не подтверждены.

Когда несовместимость уже проявилась на интеграционном стенде, разбираться приходится не только в контракте, но и в реальном обмене между клиентом и сервером. Именно эту часть можно отдельно посмотреть на бесплатном уроке Otus 22 сентября в 20:00 — «Автоматизация управления трафиком с mitmproxy»: там разберут перехват и изменение HTTP-запросов и ответы сервиса в тестовых сценариях. Участие бесплатное, можно задать вопросы преподавателю по ходу занятия.
Больше бесплатных уроков сентября можно посмотреть в дайджесте.