Выпустить релиз — часы работы команды. Упасть на старте — 1 секунда. Узнать об этом не из отзывов пользователей — бесценно.
Серверные тесты проходят, эндпоинт отвечает 200 OK, но мобильный клиент падает на первом же ответе API.
Типичный сценарий: в user.id приходит null, у status появляется новое значение или меняется вложенная структура — и ответ API расходится с клиентскими моделями.
Чтобы ловить такие расхождения до релиза, мы встроили в пайплайн контроль совместимости API и приложения. Этот слой решает две задачи: не даёт серверным изменениям ломать текущий клиент и проверяет совместимость предстоящего релиза с текущим бэкендом. При расхождении пайплайн останавливается на этапе проверки api, до build/archive/sign/publish.
Я Алексей Матвеев, директор по мобильным технологиям в «Первой Форме». В статье расскажу, как мы задали архитектуру решения и делегировали ИИ рутинные задачи. В CI/CD мы проверяем ответы API по DTO и YAML-контрактам, а для сценариев изменения состояния сверяем «до/после». Итог сразу уходит в CI-лог и тред задачи.
Что внутри:
как проверять живой API «глазами клиента» без переписывания существующих тестов;
как ловить битые данные в сценариях чтения и изменения состояния;
как формировать отчёт, который команда реально использует в ежедневной работе;
как запускать офлайн-контур (
test-configs→contracts→domains→fixtures→report) без сети и токенов.

С чего все началось
Всё началось с приземлённой задачи: у нас был скрипт сборки мобильного приложения. Он собирал нужный таргет, архивировал, подписывал, загружал артефакт и оставлял итоговый комментарий в рабочей задаче.
Мы переписали его на Python, добавили работу с веткой и отчёт об ошибках. Скрипт перестал быть чёрным ящиком: появились тайминги этапов и понятные логи падений. Так процесс стал надёжнее, при этом скриптовую версию мы оставили рядом.
Но потом возник неудобный вопрос: если живой API уже сломан для текущей версии приложения, зачем вообще запускать долгую сборку? Билд, подпись и публикация могут пройти успешно. Но если после установки приложение не открывает ни чаты, ни ленту, ни контакты, зелёный билд сам по себе не гарантирует работоспособность клиента на живом API. Так API-проверки стали отдельным этапом до сборки.

Когда мы начали думать, как это реализовать, оказалось, что все нужные детали уже есть: Swagger, API-сервисы в мобильном проекте, DTO, CI-билдер и рабочие обсуждения в продуктовых задачах.
Оставалось собрать их в единый процесс: брать живой API, проверять по контракту приложения и возвращать результат в среду, где команда работает каждый день.
Мы не строили «платформу на неделю, которую потом поддерживает один QA на полставки». Шли короткими шагами: один домен, один валидатор, один формат отчёта, один новый контекст.

Минимальный рабочий вариант собрали за день, а дальше короткими итерациями довели его до структуры, которую уже удобно расширять.
Почему серверных тестов недостаточно
Серверные тесты, безусловно, нужны — они проверяют бизнес-логику, интеграции и регрессии. Но мобильный клиент задаёт другой вопрос: сможет ли конкретная версия приложения распарсить и использовать ответ API без срочного обновления?
На практике чаще всего ломается следующее:
сервер считает поле необязательным, а DTO клиента парсит его как обязательное;
в API появляется новое enum-значение, а клиент не умеет его отобразить;
эндпоинт возвращает валидный JSON, но структура data/errors не совпадает с ожиданием метода;
поле приходит под историческим алиасом или в другом регистре;
метод в Swagger UI работает, но мобильному сценарию нужен связанный набор данных: задача, чат, ссылка, файл, тред, компактный ответ;
метод изменения состояния возвращает 200 OK, но переход невалиден для клиента: например, close/open не восстановил исходное состояние.
У веб-клиента часть таких проблем решается быстрым релизом фронтенда, но с нативным мобильным приложением так не всегда: есть хвост установленных версий и цикл публикации в магазинах. Поэтому нужен отдельный слой совместимости между живым API и клиентским контрактом — иначе получаем классический «works on my machine», только в мобильной версии это звучит как «works in CI, fails in production».
Вопрос |
Серверные API-тесты |
Контур совместимости клиента |
Кто главный потребитель проверки |
Серверная команда |
Мобильный клиент |
Что считается успехом |
Сервер выполнил бизнес-логику |
Ответ прошел DTO-контракт клиента |
Какие методы под контролем |
Обычно серверные сценарии |
Сценарии чтения и изменения состояния |
Где живут ожидания |
Серверный код, схемы, фикстуры |
API-слой приложения, DTO, правила декодирования |
Что видно в отчете |
Ошибка серверного сценария |
Метод клиента и сломанный DTO-контракт |
Как распределили роли Swagger, Postman и Pact
Коротко по ролям:
Swagger фиксирует ожидаемую форму API (схемы и поля), но не подтверждает совместимость конкретной версии клиента.
Postman/Newman покрывает API-проверки на уровне сценариев и окружений.
Pact нужен, когда требуется независимая межкомандная блокировка релиза.
Контур совместимости даёт ранний ответ по конкретной версии клиента и возвращает итог в тред задачи.
Все эти инструменты делают своё дело, но наш практический вопрос другой: может ли текущая версия мобильного клиента безопасно пройти реальные сценарии чтения и изменения состояния и сразу вернуть понятный итог в рабочий тред команды?
Проверка идёт по DTO клиента на живом API, а результат сразу попадает в рабочий контекст команды — в тред задачи и Markdown-отчёт. Это уже не папка со скриптами, а небольшой тестовый проект, который сопровождаем как код.
Далее разберём, как мы оформили этот слой в коде и структуре проекта, чтобы его было легко расширять.
Структура и домены
Первый рабочий вариант представлял собой обычный Python-проект с простой структурой:
api-tests/ run_all.py base.py contracts/ methods/ configuration/base_configuration.yml chats/chats.yml dto/ configuration.yml chat.yml domains/ test_configuration.py test_chats.py test_contacts.py test_feed.py test-configs/ default.yml
run_all.py стал точкой входа, а base.py взял на себя общую инфраструктуру: запросы, авторизацию, envelope-парсинг, контракты, отчёты и базовые валидаторы. В рабочем CI доступ к живому API берётся из защищённых секретов, а не из репозитория.
Доменные файлы намеренно остались небольшими: новый метод должно быть легко добавить в понятное место, а не разбираться в сценарии на тысячу строк.

Первые домены выбрали по важности и частоте риска:
Домен |
Что проверяем |
Зачем |
Configuration |
|
Bootstrap системных категорий и единая контрактная база для среды |
Chats |
GET-чтение + POST-изменение состояния (close/open, pin/unpin) |
Основной коммуникационный экран и безопасные переходы состояния |
Contacts |
GET список сотрудников/профиль + POST поиск |
Люди, карточки, поиск |
Feed |
|
Разные DTO для похожих ответов и проверка цепочек изменения состояния |
Отдельно пришлось признать, что API не единообразен. Часть методов возвращает envelope:
{ "data": {}, "errors": [] }
, а часть отдаёт объект сразу в корне ответа. Поэтому у каждого метода появился явный флаг uses_envelope — небольшая деталь, которая убирает много неявной магии.
Полное описание метода выглядит так:
ApiMethodSpec( name="Поиск по всем чатам", method="GET", path="/api/chats", query={ "limit": 30, "offset": 0, "queryString": "иванов", }, uses_envelope=True, validator=validate_chat_list, )
Суть в том, что все нужные детали — эндпоинт, параметры, признак uses_envelope и валидатор — собраны в одном месте. Для методов, меняющих состояние, к этой же схеме добавляется проверка данных до и после действия.
Отчетность, которая живёт в продукте
Этот формат отчётности оказался самым практичным.
Обычно всё выглядит так: тесты пишут в лог CI, CI показывает красную или зелёную кнопку, а дальше кто-то идёт читать детали. Но у нас обсуждение работы живёт в рабочих задачах продукта, поэтому и отчёт должен попадать туда же.
При каждом запуске создаётся тред в тестовой задаче, куда каждый метод пишет короткий итог:
[Chats] Поиск по всем чатам Результат: OK | Время: 0.69s | Количество: 4 GET /api/chats query: limit=30&offset=0&queryString=иванов
В конце к треду прикладывается Markdown-вложение с полным протоколом, где ошибки, если они есть, вынесены в первый блок. В отдельную задачу с отчётом уходит короткий итоговый комментарий:
API-тесты: все успешно
Запущено методов: 18
Ошибок: 0
Полный отчет во вложении.
Это снижает число переключений между CI и задачей: сначала виден короткий итог, а полный Markdown-протокол открывается только при необходимости.

Так тесты становятся рабочей диагностикой для всей команды, а не только для тех, кто читает CI-логи.
Контексты вместо одного taskId
Пока тесты только читали списки, всё было просто. Но как только появились методы с taskId, стало ясно, что одного идентификатора недостаточно: один метод нужно проверять на личной задаче, другой — на личном чате, третий — на закрытой группе, а часть методов требует прогона сразу в нескольких контекстах.
Так выглядит YAML-конфиг тестовой среды:
report_to_task_id: <report_task_id> tasks_for_test: private_chat: <private_chat_task_id> closed_group_chat: <closed_group_chat_task_id> personal_task: <personal_task_id>
API-контур задаётся через переменную API_BASE_URL в секретах CI: значение выбирается для нужного тестового контура и не хранится в репозитории.
Перед запуском тестов конфиг валидируется:
report_to_task_idзадан;все идентификаторы в
tasks_for_testуказаны и доступны;каждый идентификатор соответствует своему контексту:
private_chat,closed_group_chat,personal_task.

YAML-контракты: отдельный слой между API и валидаторами
Когда доменов и методов стало больше, одних Python-валидаторов не хватило. Мы добавили слой contracts/*.yml: сначала фиксируем форму ответа, затем накладываем проверки бизнес-инвариантов. Это не формальность: сервер и клиент эволюционируют с разной скоростью, поэтому совместимость DTO с живым API нужно контролировать отдельно.
Пример описания метода конфигурации:
name: methods.configuration.base_configuration request: method: GET path: /api/configuration response: uses_envelope: true payload: ref: dto/configuration
В dto/configuration.yml описываются конкретные поля: обязательность, nullable, типы, вложенности. Даже тест конфигурации проходит через тот же YAML-пайплайн, что и остальные домены.
Структура контрактов делится на два уровня:
в
contracts/methods/*фиксируются метод, путь, ожидаемая обёртка ответа (uses_envelope) и ссылка на DTO-схему,в
contracts/dto/*фиксируется сама структура payload с типами, обязательностью полей,nullableи вложенными объектами.
Валидация ответа работает последовательно:
Выполняем запрос по r
equest.methodиrequest.path.Приводим ответ к нужной форме в зависимости от
uses_envelope.Сверяем
payloadс DTO-схемой из YAML.При первом несовпадении этап api падает с указанием пути проблемного поля и расхождения между ожидаемым и фактическим типом.
YAML-контракты дают проверяемую точку в diff: видно, где и как изменился клиентский контракт.
Write-тесты: безопасность состояния как обязательное условие
С тестами, меняющими состояние (POST/PUT), легко испортить тестовую среду и даже не заметить этого сразу. Простой пример: сценарий close → open должен не просто закрыть чат, но и гарантированно вернуть его в исходное состояние.
Если после прогона чат остался закрытым, это не «почти успех», а ошибка самого теста. POST/PUT без проверки состояния после действия — это не полноценный тест, а скорее optimistic UI для CI: выглядит обнадёживающе, но не даёт надёжной гарантии.
Поэтому появилось жёсткое правило: любой тест с изменением состояния обязан быть безопасным и оставлять среду в том же состоянии, в котором её нашёл.
Это соответствует принципу provider states в Pact: проверка запускается в явно заданном состоянии данных, а не в «случайной» среде. В нашем случае эти состояния фиксируются через tasks_for_test и проверку «до/после» для сценариев изменения состояния.
Для comments/addмы зафиксировали детерминированный вход: тест использует заранее подготовленные данные и не зависит от внешних runtime-источников. Иначе метод начинает падать не из-за проблем с контрактом, а из-за случайных факторов окружения — для контрактного теста это лишняя нестабильность.
В офлайн-режиме методы с изменением состояния не исполняются и помечаются как «пропущено», а сценарии чтения работают на фиксированных ответах.
Контроль совместимости на живом API — это безопасно?
Это не нагрузочное тестирование и не работа по реальным пользовательским данным. Для нас обязательны следующие ограничения:
прогоны выполняются только в выделенных тестовых контекстах (
tasks_for_test) и под служебными учётными записями;для каждого сценария изменения состояния сначала фиксируем исходное состояние, затем выполняем действие и проверяем, что состояние возвращено к исходному;
проверка запускается как
prebuild-checkконкретной версии клиента; это не фоновый и не массовый прогон.
· контур и доступы задаются вне репозитория.
Как это встроилось в CI
В билдере появился отдельный этап API, который запускается самым первым:
prepare -> api -> build -> archive -> sign -> publish
Практический эффект: если API-тесты падают, сборка останавливается на этапе api, и ресурсоёмкие этапы пайплайна не стартуют.
В экосистеме Pact похожий CI-гейтинг обычно реализуют через матрицу проверок в Pact Broker и can-i-deploy; здесь тот же принцип реализован предсборочной проверкой совместимости для конкретной версии клиента.
Пример лога падения:
Совместимость API: FAILED (18 методов, 1 ошибка) этап: api метод: POST /api/comments/add контекст: private_chat #<task_id> ошибка: проверка состояния после действия не прошла детали: комментарий создан, но привязка к треду неконсистентна для мобильного клиента пайплайн остановлен: build/archive/sign/publish пропущены

В итоговом отчёте сборки фиксируются ветка, таргет, версия приложения, время начала и окончания, тайминги этапов и краткий анализ ошибки.
Если разработчик вручную прерывает прогон, комментарий в рабочую задачу не отправляется, чтобы не создавать ложный сигнал.
Где граница этого слоя
Чтобы не смешивать инструменты по назначению, мы формулируем критерий так: может ли текущий мобильный клиент распарсить и использовать реальные ответы текущего API?
Это не замена API-версионированию, а дополнительный уровень контроля общей клиент-серверной архитектуры: он подтверждает, что изменения бэкенда сохраняют рабочее поведение уже выпущенных сборок и предстоящего релиза. Входное условие здесь — принятая политика обратной совместимости API; этот контур технически проверяет, что она реально соблюдается.
Что этот слой не делает:
не заменяет сквозные и интерфейсные тесты;
не валидирует бизнес-логику сервера;
не заменяет межкомандное контрактное управление.
Остальные инструменты никуда не исчезают — они просто ловят другие классы ошибок:
Слой |
Что хорошо ловит |
Что не закрывает |
серверные тесты |
бизнес-правила сервера |
ожидания конкретной версии клиента |
Проверка Swagger |
грубое соответствие схеме |
фактический парсинг DTO |
Postman/Newman |
проверка API-сценариев и исследование поведения |
связь с DTO клиента, доменная расширяемость, отчетность внутри продукта |
UI-тесты мобильного клиента |
пользовательский сценарий |
точную причину проблемы API |
мок-сервер |
стабильность UI-тестов |
состояние живого сервера |
Модульные тесты (unit-тесты) |
локальная логика клиента, DTO-модели, маппинг |
совместимость с живым API и серверный дрейф контрактов |
Контур совместимости клиента |
контракт реального API глазами клиента |
визуальные и UX-регрессии |
Модульные тесты и контроль совместимости не конкурируют — это два слоя с разными задачами:
Главная задача: Pact — контрактная дисциплина между командами
consumerиprovider; наш контур — предсборочная проверка совместимости мобильного клиента с живым API.Ключевая роль в CI: в Pact это межкомандная блокировка релиза через матрицу проверок в Pact Broker (реестре контрактов и результатов проверок). Наш контур останавливает пайплайн до ресурсоёмких этапов и сразу даёт материал для разбора причин расхождений клиента и API.
Это не замена одного другим: Pact и наш контур дополняют друг друга.
Если проверка фиксирует расхождение, команды совместно решают следующий шаг: откат/исправление API или точечная правка клиентского контракта. Второй вариант допустим только когда изменение безопасно для стабильности мобильного приложения и не ломает уже выпущенные сборки.
Почему подход легко расширять
Подход расширяется по одному маршруту: инфраструктурные вопросы уже закрыты, поэтому новый метод добавляется без пересборки процесса.
Маршрут добавления нового метода:
Найти метод в локальной REST-карте или Swagger-кэше.
Найти DTO и реальные правила декодирования в мобильном проекте.
Зафиксировать контракт метода в YAML.
Выбрать домен тестов или создать новый.
Описать метод в тестовой спецификации (
GET/POST/PUT) и его параметры.Подключить контрактную проверку и ручной валидатор по модели клиента.
Для сценария изменения добавить безопасную цепочку: проверка до действия → действие → проверка после действия.
Получить результат в треде и Markdown-отчёте.

В git зафиксированы артефакты контура: REST-карта клиента, Swagger-кэш, правила конфигурации, YAML-контракты методов/DTO и правила сценариев изменения состояния. Это делает ревью предметным: в diff видно не только изменения в Python-коде, но и то, как меняются ожидания клиента.
Это не абсолютная гарантия. Но это понятный маршрут: новый API-риск добавляем в процесс проверки, а не ловим вручную.
Что получилось и как повторить
В текущем виде подход покрывает четыре основных домена и предсборочную рутину:
Срез |
Что проверяем |
Конфигурация |
Параметры среды, системные категории и YAML-контракт |
Чаты |
Чтение, поиск, счетчики, настройки, pin/unpin, close/open |
Контакты |
Список сотрудников, поиск, профиль |
Лента |
Лента, комментарии, треды, закрепленные комментарии |
Сценарии изменения |
|
Предсборочная рутина |
Этап api отсекает проблемные сборки до ресурсоёмких этапов пайплайна |
Покрытие можно расширять — задачи, файлы, сообщения, подписи, календарь, почта. Новый домен подключается без переписывания запускающего скрипта и отчётности.
Если повторять подход у себя, достаточно трёх шагов. Сначала выберите 3–5 самых болезненных мобильных методов и зафиксируйте для них минимальные YAML-контракты с DTO-валидацией. Затем поставьте проверку совместимости перед дорогими стадиями сборки и отправляйте отчёт в привычный рабочий канал команды.
Это даёт быстрый результат без «гигантских платформ», которые потом сложно поддерживать.
Где помог ИИ, а где решала инженерия
В этом проекте ИИ стал рабочим маппером между Swagger-описанием и DTO мобильного клиента. На вход давали JSON-ответы живых эндпоинтов и структуры Swift/Kotlin, на выходе получали черновики YAML-контрактов с полями, типами и опциональностью.
Это сняло самую трудоёмкую ручную часть на больших вложенных схемах. После фиксации правил uses_envelope и маппинга ошибок покрытие на новые домены расширялось заметно быстрее, а отчёты между итерациями приводились к единому формату.
Финальное решение оставалось за инженером: каждый контракт проверяли живым вызовом API и отдельно валидировали сценарии изменения состояния «до/после». Так отлавливали типичные ошибки генерации (в том числе галлюцинации) на больших структурах: неверную опциональность, несовпадение типов и лишние поля.
5 правил, которые реально изменили процесс
Тесты изменения состояния (write) — только с детерминированным входом. Результат: меньше шума окружения и ложных срабатываний. Проверяем контракт, а не случайное состояние среды.
Проверка совместимости — до дорогих стадий сборки. Результат: ошибки ловятся до ресурсоёмких этапов пайплайна. «Зелёный CI» перестаёт быть иллюзией безопасности.
Формат ответа фиксируется явно. Результат: догадки (
data/errorsvsroot object) исчезают. Флагuses_envelopeзакрывает класс скрытых расхождений контракта.Покрытие расширяем итеративно. Результат: один домен — одна итерация. Система растёт без переписывания раннера.
Отчёт живёт в рабочем пространстве команды. Результат: меньше переключений между CI и трекером. Короткий итог — в тред, полный Markdown-протокол — во вложении.
Что изменилось на практике: выводы и границы
Этот слой стал предсборочным фильтром совместимости клиента с API: расхождения видны до выпуска артефакта, а разбор сразу попадает в рабочий контекст команды.
Границы применения: подход требует детерминированных сценариев изменения состояния, выделенных тестовых контекстов и актуальных контрактов.
Порог входа невысокий: 3 сценария чтения и 1 сценарий изменения состояния дают первый рабочий контур за 2–3 часа.
Демо для быстрого старта: репозиторий GitHub
Вопрос к коллегам: какие проблемы совместимости API с разными сборками приложения у вас чаще всего долетают до релиза?