23 окт. 2025 г.·6 мин чтения

Фикстуры-снимки API, которые останавливают расхождение между вебом и мобильным приложением

Фикстуры-снимки API дают веб- и мобильным командам один набор реальных примеров ответов, чтобы они замечали расхождение полей раньше, а не находили его на стейджинге.

Фикстуры-снимки API, которые останавливают расхождение между вебом и мобильным приложением

Почему расхождение полей превращается в баг на стейджинге

Расхождение полей обычно начинается с мелочи. Бэкенд-разработчик переименовывает avatarUrl в profileImage, добавляет вложенный объект или меняет null на пустую строку. API по-прежнему возвращает 200. Тесты по-прежнему проходят. Один клиент может вообще ничего не заметить.

Потом стейджинг превращается в сессию поиска виноватого.

Веб-приложение может продолжать работать, потому что его маппер уже умеет обрабатывать оба названия поля. Мобильное приложение может сломаться, потому что оно всё ещё ждёт старую структуру, или потому, что его цикл релизов отстаёт и в нём до сих пор код прошлой недели. Один и тот же endpoint, один и тот же пользователь, разный результат.

Именно поэтому общие баги API ощущаются таким хаосом. Ничто не ломается явно и аккуратно. Бэкенд говорит, что ответ правильный. Веб говорит, что экран загружается. Мобильное приложение показывает краш или отсутствие данных. И все могут быть правы.

Документация редко ставит точку в споре. Обычно она описывает payload, который команда собиралась выпустить, а не тот, который на самом деле ушёл в продакшен вчера. Поле, изменённое во время хотфикса, может так и не попасть в спецификацию. Необязательные поля создают ещё одну проблему. Одна команда читает «необязательное» как «иногда отсутствует». Другая читает это как «всегда присутствует, возможно, null».

Поэтому команды тратят время не на ту работу. Они сравнивают скриншоты со стейджинга, листают старые переписки и спорят, живёт ли баг в парсинге, кэшировании или в API. Они спрашивают, какой пример ответа настоящий. Всё это мало помогает.

Цена быстро растёт, когда веб и мобильное приложение релизятся по разным графикам.

Общий источник истины решает большую часть этих проблем. Вместо споров о документации, скриншотах или памяти команды могут открыть настоящий сохранённый ответ, который оба клиента используют в разработке и тестировании. Именно здесь помогают snapshot-фикстуры. Они фиксируют то, что бэкенд реально возвращает, поэтому расхождение полей всплывает заранее, а не тогда, когда кто-то находит его на стейджинге в 18:00.

Что должна включать snapshot-фикстура

Фикстура должна выглядеть как настоящий API-обмен, а не как приглаженная демо-версия. Если мобильное приложение отправляет запрос, а сервер возвращает ответ, сохраняйте обе части вместе. Так у веба и mobile будет одна и та же точка отсчёта, а маленькие изменения станут заметны раньше, чем превратятся в баг на стейджинге.

Полезная фикстура обычно включает HTTP-метод, endpoint, query-параметры, тело запроса, код ответа и тело ответа. Сохраняйте и заголовки, которые влияют на поведение. Content type, форма auth, локаль, версия и feature flags могут менять ответ. Если их убрать, пример перестаёт совпадать с реальностью.

Команды часто делают фикстуры слишком аккуратными. Они убирают поля null, пропускают пустые массивы, упрощают вложенные объекты или заменяют реальные значения расплывчатыми заглушками. Именно там и прячется расхождение. Клиент может сломаться, потому что middle_name равен null, roles — это пустой массив, или preferences.notifications.email ушло на один уровень глубже. В фиктуре нужны и такие неудобные детали.

Одного удачного примера недостаточно. Сохраняйте небольшой набор, который отражает, как endpoint работает на практике: обычный успешный сценарий, ответ с null, один с пустыми списками или объектами, один с отсутствующими необязательными полями и один там, где клиенты читают глубоко вложенные данные.

Используйте реальный трафик, когда это возможно. Реальные payload'ы несут сочетания, которые рукописные примеры часто не показывают. Личные данные убирайте, но структуру не очищайте. При необходимости заменяйте имена, email, токены и ID, сохраняя форматы. Если реальное значение — UUID, оставляйте UUID. Если поле иногда бывает пустой строкой, оставляйте и это тоже.

Фикстура профиля не должна заканчиваться на name и email. Сохраняйте поля аватара, nullable-номер телефона, пустые списки команд, объекты настроек и серверные метаданные, если клиенты их получают. Смысл простой: зафиксировать то, что API реально говорит, включая те части, которые никто не замечает, пока приложение не упадёт или не отрисует экран неправильно.

Если фикстура выглядит немного грязной, это обычно хороший знак. Реальные payload'ы и правда грязные.

Какие ответы API сохранять в первую очередь

Начинайте с тех ответов, которые ломают приложение самым заметным образом, если в них меняется одно поле. Логин, профиль, биллинг и результаты поиска обычно стоят в начале списка. Их читают и веб, и mobile, они появляются рано в пути пользователя, а маленькое изменение схемы может создать много шума.

Логин заслуживает особого внимания, потому что ошибки auth блокируют всё, что идёт после него. Переименуйте поле токена, уберите значение срока действия или заверните часть ответа в новый объект — и один клиент может оказаться наполовину авторизованным. Такие расхождения часто проходят мимо, потому что каждая команда тестирует только свой happy path.

С payload'ами профиля проблемы возникают по другой причине. Они со временем разрастаются. URL аватара, настройки локали, feature flags, статус подписки, роли в команде и необязательные поля для новых функций быстро накапливаются. Один экран в mobile может всё ещё ожидать phone, а веб-приложение уже читает phone_number. Реальная фикстура ловит это до того, как кто-то начнёт гадать, кто что сломал.

Биллинг стоит брать в работу раньше, даже если им пользуется только часть продукта. Денежные поля, состояния тарифов, даты пробного периода и статус счёта влияют на paywall'ы, подсказки об апгрейде и страницы аккаунта. Когда поле меняет тип с числа на строку или уходит под вложенный объект, клиенты ломаются особенно неприятно.

Фикстуры для поиска нужны раньше, чем многие команды ожидают. Поисковые ответы смешивают ID, названия, миниатюры, бейджи, пагинацию, данные ранжирования и удивительно много nullable-полей. Одно пропавшее изображение или изменённый тип результата может сломать карточки, фильтры или области нажатия на mobile.

Простое правило помогает: начинайте с endpoint'ов, которые оба клиента используют каждый день, с потоков, которые блокируют вход, оплату или навигацию, и с payload'ов с необязательными, вложенными или быстро меняющимися полями. Админские и внутренние endpoint'ы оставьте на потом. Если какой-то внутренней странице нужен endpoint раз в месяц, он может подождать.

Фикстуры приносят пользу быстрее всего там, где защищают общие ответы, к которым пользователи обращаются постоянно.

Как собирать фикстуры из реального трафика

Начните с ответа из стабильной среды, а не с вручную собранного примера. Реальный payload хранит детали, на которых клиенты спотыкаются: пустые массивы, null, необязательные поля, старые значения enum и вложенные объекты, которые казались безобидными, пока одно приложение не разобрало их иначе.

Чаще всего лучший пример даёт production, но только если его можно безопасно зафиксировать. Если это кажется рискованным, используйте QA- или pre-release-среду с заполненными данными, которая максимально похожа на реальные. Берите endpoint'ы, которые часто меняются или питают больше одного клиента.

Перед тем как что-то сохранить, хорошо обезличьте данные. Уберите токены, session ID, email-адреса, номера телефонов, внутренние заметки и любые личные данные. Если ID может раскрыть запись клиента, замените его на фейковое, но правдоподобное значение. Форму сохраняйте. Фикстура должна по-прежнему выглядеть реальной, просто без всего чувствительного.

Имена файлов важнее, чем кажется. Добавляйте endpoint или имя домена, короткий сценарий вроде profile-basic или profile-with-subscription, при необходимости дату захвата и сохраняйте файл как обычный JSON. Имя вроде profile-with-subscription.2026-04-12.json скучное, и именно поэтому оно работает.

Потом добавьте один простой тест. Пусть API отдаёт текущий payload для того же сценария и сравнивает его с сохранённой фикстурой. Если ответ изменился, тест должен упасть и показать diff. Это ловит расхождение полей до того, как команды веба и mobile потратят полдня на отладку стейджинга.

Не обновляйте файл автоматически, когда этот тест падает. Сначала проверяйте каждый diff. Какие-то изменения правильные, например новое поле или переименованный enum. Какие-то — тихие поломки, например отсутствующее свойство или число, которое стало строкой. Решать должен человек.

Если у вас уже есть хорошие логи и observability, это становится намного проще. Oleg Sotnikov часто говорит о практических инженерных системах на oleg.is, и это хороший пример того, почему они важны: когда команды могут быстро и безопасно собирать реальные образцы, они превращают их в проверки, которые остаются полезными от релиза к релизу.

Где хранить фикстуры, чтобы ими пользовались обе команды

Получите поддержку Fractional CTO
Получите опытную CTO-помощь для проектирования API, привычек поставки и координации команды.

Команды перестают доверять фиктурам, когда они лежат в репозитории одного приложения, а всем остальным приходится копировать их вручную. Храните их там, где обе команды уже ожидают увидеть общую истину: рядом с файлами API-схемы или shared API-тестами. Когда бэкенд-разработчик меняет ответ, фикстура должна быть достаточно близко, чтобы он увидел её в том же pull request.

Если web и mobile живут в разных репозиториях, храните фикстуры в репозитории, который задаёт контракт, и подтягивайте эту папку в оба набора тестов. Один источник лучше двух почти одинаковых копий. Как только команды начинают форкать файлы фикстур, расхождение возвращается.

Используйте одинаковую структуру папок и одни и те же имена для каждого клиента. Инженер mobile и инженер веба должны уметь сказать «используй users/get-profile/success.basic.json» и иметь в виду один и тот же файл. Избегайте размытых имён вроде new.json, latest.json или final2.json. Такие названия очень быстро устаревают.

Хорошо работает простая структура:

fixtures/
  users/
    get-profile/
      success.basic.json
      success.missing-avatar.json
      error.not-found.json
  billing/
    get-subscription/
      success.active.json

Группируйте файлы сначала по endpoint'у, потом по сценарию. Так легко быстро ответить на два вопроса: какой payload возвращает этот endpoint и какие странные случаи мы уже покрыли?

Командам также нужно одно короткое правило для обновлений, иначе фикстуры превращаются в редактируемые примеры, которым никто не верит. Держите его простым: обновляйте фикстуру только тогда, когда API изменился намеренно, и делайте это изменение в том же pull request, где есть backend-изменение и хотя бы одно обновление теста. Если кто-то хочет поправить фикстуру без этих двух вещей, он должен остановиться.

Это правило не только поддерживает порядок в файлах. Оно вырабатывает у обеих команд один и тот же привычный подход. Когда на стейджинге появляются странные баги, они сначала проверяют одни и те же payload'ы, а не спорят о том, чей пример новее.

Простой пример профиля

Ответ профиля — хорошая точка старта, потому что и веб, и mobile обычно касаются его рано. Сохранённая фикстура может выглядеть так:

{
  "id": "user_1842",
  "name": "Maya Chen",
  "avatar_url": "https://cdn.example.com/avatars/user_1842.png",
  "plan": "pro"
}

Оба клиента читают одну и ту же структуру. Веб-приложение использует avatar_url в меню аккаунта. Мобильное приложение использует его на экране профиля и в комментариях.

Теперь представьте чистку на бэкенде. Разработчик переименовывает avatar_url в avatar, потому что так короче, и выкатывает изменение. Новый ответ выглядит так:

{
  "id": "user_1842",
  "name": "Maya Chen",
  "avatar": "https://cdn.example.com/avatars/user_1842.png",
  "plan": "pro"
}

Если запустить snapshot-тесты на сохранённой фикстуре, падение будет мгновенным. Тест покажет, что avatar_url отсутствует, а avatar — лишнее поле. Это произойдёт в CI, до того как кто-то откроет стейджинг и начнёт искать, куда пропали фотографии профиля.

В этом и состоит практическая ценность фикстур. Они ловят расхождение полей, пока изменение ещё дёшево исправить. Бэкенд-разработчик может вернуть старое поле, какое-то время отправлять оба поля в одном релизе или сразу сообщить обеим командам клиентов, что контракт поменялся.

Профит небольшой, но реальный. Веб и mobile один раз обновляют поле, принимая одно общее решение, вместо того чтобы находить поломку в отдельных кругах.

В здоровом процессе изменение на бэкенде ломает тест фикстуры, команда договаривается о новом имени поля, web и mobile обновляют свои модели в одном рабочем цикле, а фикстура обновляется только после того, как все приняли новый ответ.

Без такой общей фикстуры команда веба может чинить стейджинг во вторник, а команда mobile наткнётся на тот же баг в четверг, но уже на другом экране. В итоге вы дважды платите за одно и то же переименование.

Ошибки, из-за которых фикстуры бесполезны

Проверьте общие API-контракты
Oleg может провести аудит быстро меняющихся endpoint'ов вроде login, profile, billing и search.

Фикстура перестаёт работать, когда выглядит аккуратно, но врёт. Самая частая проблема — JSON, придуманный вручную и никогда не приходивший ни из production, ни из реальной тестовой среды. Он может выглядеть чище живого payload'а, но именно поэтому не замечает странное поле, значение null, пустой массив или неожиданный enum, которые позже ломают клиент.

Ещё одна плохая привычка — относиться к обновлениям как к уборке. Кто-то пересоздаёт файл, видит большой diff и одобряет его, не читая внимательно. Так расхождение полей прячется на виду. Если display_name стал name или вложенный объект ушёл на один уровень глубже, фикстура сделала свою работу. Команда её проигнорировала.

Команды также делают фикстуры слишком вежливыми. Они сохраняют один обычный пример, где есть каждое поле и каждое значение выглядит идеально. Реальные общие payload'ы редко бывают такими аккуратными. Более удачный набор включает неудобные случаи: отсутствующие необязательные поля, старые значения, которые всё ещё встречаются в реальности, очень длинные строки, пустые списки, неполные профили и флаги, которые меняют поведение на одном клиенте, но не на другом.

Имена файлов тоже имеют значение. Папка, полная файлов profile.json, profile-new.json и profile-final.json, скрывает поломки вместо того, чтобы их документировать. Имя должно говорить, что это за payload и зачем он нужен. profile-v2-missing-avatar.json говорит обеим командам больше ещё до того, как они откроют файл.

Отдельные копии создают более медленный тип поломки. Веб хранит одну фикстуру, mobile — другую, а бэкенд — третий пример в другом репозитории. Через месяц никто уже не знает, какая из них всё ещё совпадает с API. Один общий источник истины менее эффектен, но он предотвращает очень много багов на стейджинге.

Фикстуры становятся бесполезными и тогда, когда теряют контекст. Сырой payload без кода ответа, endpoint'а, параметров запроса или версии схемы оставляет слишком много места для догадок. Когда появляется баг, команда тратит время на вопрос, откуда взялся пример, вместо того чтобы проверить несовпадение.

Простой пример делает проблему очевидной. Если mobile-тесты проверяют фикстуру, где avatar_url всегда строка, а production иногда отправляет null, приложение может упасть только на стейджинге. Ответ не в ещё одном вручную отредактированном файле. Ответ — реальные снимки, понятные имена, проверенные diff'ы и один общий набор, которым пользуются оба клиента.

Быстрые проверки перед релизом

Ужесточите проверки перед релизом
Добавьте простые diff-проверки ответов и правила ревью, чтобы ловить поломки раньше.

Diff фикстуры может за две минуты показать, безопасен релиз или он, скорее всего, сожжёт день на стейджинге. Секрет в том, чтобы читать изменённые payload'ы в контексте релиза. Если команда трогала только billing, а фикстура пользовательского профиля теперь показывает три переименованных поля и новый вложенный объект, остановитесь и спросите почему.

Маленькие изменения вызывают самые неприятные баги на клиентах. Поле, которое меняется с null на пустой список или исчезает, когда данных нет, может сломать один клиент, пока другой выглядит нормально. Общим payload'ам нужны скучные, повторяемые проверки, потому что расхождения прячутся в крайних случаях, а не в happy path.

Делайте проверку короткой и одинаковой каждый раз. Сравнивайте diff'ы фикстур с реальной работой релиза. Внимательно смотрите на значения null, пустые массивы, пустые строки и необязательные поля, которых может не быть. Запускайте оба клиента на одной и той же обновлённой фикстуре и убедитесь, что они парсят её одинаково. Потом пусть один ревьюер прочитает каждую изменённую строку фикстуры построчно, даже если diff выглядит маленьким.

Последний шаг важнее, чем ожидают команды. Разработчики бегло смотрят diff'ы кода. JSON они тоже бегло просматривают. Один внимательный ревьюер часто замечает неприятную часть: переименованное значение enum, изменение формата timestamp или поле, которое ушло на один уровень глубже.

Когда кто-то одобряет изменение фикстуры, фразы «вроде нормально» недостаточно. В ревью должно быть указано, что изменилось и почему это соответствует релизу. Например: «добавили middle_name в payload клиента, mobile игнорирует неизвестные поля, web их отображает, но не требует». Такие заметки потом очень ускоряют отладку.

Если фикстура изменилась, а никто не может объяснить почему, считайте это багом, пока не доказано обратное. Это звучит строго. Но всё равно дешевле, чем выяснить на стейджинге, что веб и mobile теперь по-разному понимают один и тот же ответ.

Что делать дальше

Начните с малого. Возьмите один общий endpoint, который уже используют и веб, и mobile, и создайте две-три фикстуры из реального трафика на этой неделе. Профиль, сессия или checkout-ответ обычно достаточно, чтобы доказать процесс.

Используйте примеры, которые отражают обычную неидеальность, а не отполированные demo-данные. Сохраните один обычный случай, один крайний случай и один старый ответ, если клиенты всё ещё его видят. Именно там расхождение полей часто появляется первым.

Для этого не нужна большая платформа. Даже маленькая CI-проверка помогает. Если бэкенд убирает поле, переименовывает его или меняет тип с null на пустую строку, сборка должна падать до того, как команды веба и mobile начнут терять время на поиски в стейджинге.

Напишите одно короткое правило обновления и держите его рядом с фикстурами. Формулируйте просто: когда кто-то меняет общую структуру ответа, он обновляет фикстуру в том же pull request и добавляет одну заметку о влиянии на клиента. Одно такое правило убирает очень много догадок.

Фикстуры работают лучше всего, когда правилом владеет один человек, а пользуются им обе команды. Если за файлы никто не отвечает, они быстро устаревают. Если одна команда прячет их в своём репозитории, другая перестаёт им доверять.

Цель — скучные релизы. Бэкенд добавляет marketing_preferences в payload профиля, фикстура меняется в том же pull request, сборка проходит, и обе клиентские команды видят новое поле ещё до стейджинга.

Если ваша команда настраивает это всё параллельно с работой над архитектурой, поставкой или контрактами клиентов, Oleg Sotnikov предлагает такой практический Fractional CTO-help через oleg.is. Важен не сама фикстура. Важен процесс, который обе команды действительно будут продолжать использовать.

Часто задаваемые вопросы

Что такое snapshot-фикстура?

Snapshot-фикстура — это сохранённая копия реального API-запроса и ответа для одного сценария. Она даёт веб-, мобильной и бэкенд-командам один и тот же payload для тестов, поэтому переименованное поле или изменённый тип всплывают в CI, а не на стейджинге.

Почему нельзя полагаться только на API-документацию?

Документация показывает, что команда собиралась выпустить. Фикстуры показывают, что сервер реально вернул. Если хотфикс поменял avatar_url на avatar, а спецификацию никто не обновил, фикстура поймает несоответствие намного раньше.

Что должно быть в хорошей фикстуре?

Сохраняйте весь обмен, а не только красивую часть. Записывайте HTTP-метод, endpoint, query-параметры, тело запроса, код ответа, тело ответа и любые заголовки, которые меняют поведение, например форму авторизации, локаль, версию или feature flags.

Нужен ли мне больше одного примера на эндпоинт?

Да. Один happy path пропускает большую часть расхождений. Сохраняйте обычный успешный сценарий, один с null, один с пустыми массивами или объектами, один с отсутствующими необязательными полями и один с более глубоко вложенными данными, если клиенты их читают.

Какие эндпоинты сохранять первыми?

Начните с endpoint'ов, которые оба клиента используют каждый день, и с потоков, которые быстро блокируют пользователя. Логин, профиль, биллинг и поиск обычно дают самый быстрый эффект, потому что маленькие изменения схемы там создают много шума.

Лучше использовать реальный трафик или JSON, написанный вручную?

Используйте реальный трафик, когда можете. Вручную написанный JSON обычно убирает неудобные детали, из-за которых и ломаются клиенты: пустые массивы, старые значения enum и странную вложенность. Бери́те данные из production или близкой QA-среды, а потом убирайте секреты, не меняя структуру.

Как убрать чувствительные данные, не потеряв пользу?

Заменяйте имена, email, токены, номера телефонов и ID записей на фейковые значения, которые сохраняют тот же формат. Если в живом поле лежит UUID, оставьте UUID. Если API иногда присылает пустую строку или null, оставьте и это тоже.

Где хранить фикстуры?

Храните одну общую папку с фикстурами рядом с API-контрактом или backend-тестами, а не только внутри репозитория одного клиента. Веб и mobile должны подтягивать одни и те же файлы, использовать одни и те же имена и не держать отдельные копии, которые расходятся.

Когда обновлять фикстуру?

Обновляйте фикстуру только тогда, когда API изменился намеренно. Кладите изменение backend'а, изменение фикстуры и изменение теста в один и тот же pull request. Пусть тест падает на неожиданных diff'ах, а потом уже смотрите diff перед merge.

Какие ошибки делают фикстуры бесполезными?

Команды портят фикстуры, когда придумывают чистые демо-пейлоады, автоматически одобряют большие diff'ы, держат отдельные копии или вырезают контекст вроде кода ответа и параметров запроса. Как только файл перестаёт совпадать с реальным трафиком, ему перестают доверять, и баги на стейджинге возвращаются.