В мире микросервисов и сложной продуктовой логики наступает момент, когда классические E2E-тесты превращаются в проблему. Количество пользовательских сценариев исчисляется сотнями, каждый новый конфиг требует десятков тестов, а регресс вместо быстрой проверки становится многочасовым процессом.
Но если сценарий становится нелинейным (появляются развилки, выбор пользователя ведёт на разные экраны) всё усложняется. С этим E2E-тесты ещё справляются: пишем несколько тестов, каждый под свой путь. Сложнее, но решаемо.
Мы столкнулись с этим при работе с платформой в Услугах Авито: пользователь заполняет форму заявки, переходя между экранами. Логика переходов между экранами зависела от категории услуги, типа экрана и выбранных опций. Сценариев стало попросту слишком много. Пришлось искать другой путь.
Меня зовут Константин Горностаев, QA в Авито, в этой статье я расскажу о подходе, который позволил нам решить эту задачу и получил название «система фейков».
В этой статье

Когда классические E2E-тесты упираются в потолок
Представьте сто категорий услуг: «Мастер на час», «Вывоз мусора», «Ремонт домов». Для каждой — свой набор экранов, свои условия переходов. Типов экранов ограничено (10–11), но их комбинации дают сотни уникальных пользовательских маршрутов. Покрыть это классическими E2E-тестами — значит плодить сотни сценариев, каждый под свой путь. Таблица «категория × тип экрана» превращается в «ад масштабирования».

Это приводит к вопросам:
Как проверить, что изменение в одном типе экрана не сломало все сценарии, где он участвует?
Как убедиться, что связка «жёлтый экран → розовый экран» работает, а «желтый → синий» — нет?
Как отловить рекурсию после возврата на предыдущий шаг?
И главное — как тестировать это без человеко-недель регресса?
Мы посмотрели, есть ли готовые инструменты внутри компании. На тот момент решений, которые покрывали бы регресс сложных пользовательских сценариев в разных микросервисах в зависимости от данных, не оказалось.
Пришлось думать самим. Пирамида тестирования (юниты, интеграционные тесты, UI) остаётся, но между интеграционными тестами и полноценными E2E на проде образовалась зона, где не хватало инструмента, который позволяет тестировать продакшн-код в изоляции, на реальных данных, без развёртывания инфраструктуры и множества тестовых пользователей.
Так появилась система, которую мы назвали «система фейков».
Что такое система фейков и зачем она нужна
Мы искали способ проверять продакшн-код целиком, но без реальных баз, соседних сервисов и тяжелой инфраструктуры и без тестовых пользователей на проде, которые потом случайно засветятся в аналитике.
Итогом стала система фейков — подход к сквозному тестированию, который работает на любом окружении, но:
не требует реальных вызовов внешних сервисов и баз данных;
не поднимает дополнительные инстансы;
использует настоящий продакшн-код, а не упрощенные версии.
Суть простая: берем реальный обработчик из продакшн-сервиса, запускаем в изолированной среде, подставляем заранее подготовленные ответы от всех его зависимостей и смотрим на результат. Сам обработчик выполняется целиком — со всей бизнес-логикой, преобразованиями и внутренними вызовами. Меняется только то, что снаружи: базы, другие микросервисы, внешние API.
Например, хендлер next_page вызывает 5 внешних сервисов. Мы подменяем все пять заранее подготовленными JSON-ответами и проверяем, что хендлер вернул правильный следующий экран.
Этот подход находится между интеграционными тестами (проверяют один слой) и классическими E2E (проверяют всё, но с реальными зависимостями). Он позволяет быстро прогонять сложные сценарии, не нагружая инфраструктуру и не задевая реальных пользователей.
Что это дает? Теперь можно нажать одну кнопку и за секунды проверить, что новый конфиг или изменение в коде не сломали ни один из сотни сценариев. Получать обратную связь до выкатки в прод без проблем с тестовыми данными и стабильностью окружений.
В следующем разделе разберём, почему для этого подошли именно фейки, а не моки, и чем они отличаются.

Почему «фейки», а не «моки»
Когда мы начали обсуждать реализацию, возник естественный вопрос: зачем придумывать что-то новое, если для изоляции зависимостей давно существуют моки? Это стандартный инструмент в тестировании. Но для нашей задачи классические моки не подошли.
Моки имитируют поведение зависимостей. Они запоминают, какие методы вызывались, сколько раз, с какими аргументами. В тесте можно проверить, что нужный вызов произошел, а другой — нет. Это работает, когда важно проконтролировать взаимодействие между компонентами.
Но моки не содержат бизнес-логики. Если зависимость сложная, с несколькими методами, которые влияют друг на друга, моки не помогут воспроизвести реальное поведение. Придется вручную прописывать цепочки вызовов в каждом тесте.
Фейки — это упрощенные, но работающие реализации зависимостей. Они содержат внутри себя логику (пусть и упрощённую) и ведут себя как настоящий сервис в рамках теста. Фейк может хранить состояние, возвращать разные ответы в зависимости от входных данных, имитировать ошибки.
Для нашей системы это оказалось критичным. Сценарии, которые мы тестируем, — длинные цепочки переходов между экранами. Каждый переход — вызов сервиса, который внутри связан с другими сервисами. Чтобы пройти всю цепочку, недостаточно заставить один хендлер вернуть фиксированный ответ. Нужно, чтобы все зависимости на всём пути вели себя согласованно.
Если бы мы использовали классические моки, для каждого сценария пришлось бы вручную описывать поведение всех зависимостей на каждом шаге. Получился бы огромный объём повторяющегося кода. При изменении API зависимостей — массовое переписывание тестов.
Фейки позволяют один раз написать упрощённую реализацию клиента к сервису-зависимости, а потом использовать её во всех сценариях. Эта реализация знает, как обрабатывать разные входные параметры, как отвечать на последовательные вызовы. В тестах мы только задаём начальные данные (какие ответы подставлять), а фейк сам решает, когда и что вернуть.
Получается, что система фейков — это способ собрать из готовых фейков цепочку, которая ведёт себя как реальная система, но не требует поднятия инфраструктуры и не создаёт побочных эффектов.
В нашем случае фейки используются на двух уровнях. В сервисе оркестрации мы генерируем фейки для зависимостей каждого шага сценария. В конечных сервисах эти фейки подставляются через middleware, заменяя реальные вызовы. Так мы получаем стабильное, детерминированное окружение, в котором можно прогонять любые сценарии.
Моки остались там, где они нужны — для проверки, что определенные вызовы действительно произошли. Но основу системы составляют именно фейки.
Архитектура системы
Система состоит из трёх сервисов и библиотеки. Два из них — координатор и библиотека — универсальны и не знают о домене.
Третий — оркестратор — специфичен для продукта и генерирует тест-кейсы. Вместе они образуют конвейер, который превращает описание пользовательских сценариев в готовые прогоны с проверкой результатов.

1. Сервис оркестрации тест-кейсов отвечает за генерацию тестов. На входе он получает «сырые данные» — описание сценариев продукта. В нашем случае это JSON-файл с экранами, их типами, полями, вариантами выбора и правилами переходов.
Сервис анализирует конфиг и генерирует тест-кейсы: определяет последовательность экранов с учётом условий переходов, формирует входные данные для каждого шага, ожидаемый результат и набор ответов от зависимостей — какие JSON-ответы должны вернуть вызовы в соседние сервисы. На выходе - структурированный набор тест-сьютов. Сам сервис оркестрации не знает, как выполняются тесты, он только готовит данные.
2. Сервис-координатор. Универсальный посредник. Принимает тест-сьют и выполняет все вызовы, которые в нём описаны. Координатор получает имя сервиса, имя хендлера, номер шага и структуру тест-кейса.
Затем — рефлексия. На этапе компиляции координатор не знает, какие сервисы и хендлеры придется вызывать. Он динамически находит нужный метод и вызывает его с подготовленными аргументами. Благодаря этому координатор остается универсальным: новые сервисы подключаются без изменения его кода. Достаточно, чтобы в них был реализован тестовый хендлер.
Пример кода:
``` // Координатор находит нужный метод через рефлексию client := h.clients[suite.ClientName] method := reflect.ValueOf(client).MethodByName(suite.HandlerName) // Создает входную структуру и заполняет данными из тест-кейса inValue := reflect.New(method.Type().In(1).Elem()) // ... заполнение полей Input, Output, Interactions // Вызывает метод results := method.Call([]reflect.Value{reflect.ValueOf(ctx), inValue}) ```
3. Библиотека создаёт middleware, который перехватывает исходящие RPC-вызовы и подставляет подготовленные ответы.
``` // Две строки - и клиент к хранилищу подменён mwFactory := autotestswrapper.ClientMiddlewareFactory(in.Interactions) storageClient := briefrpc.ClientFor[storage.Server]( mwFactory.Option(storage.Name), ) ```
Middleware перехватывает вызов к внешнему сервису и передаёт управление тому, что подставили. В терминологии тестовых объектов (https://martinfowler.com/articles/mocksArentStubs.html, Martin Fowler) это может быть что угодно: заглушка, фейк или мок.
Заглушки возвращают фиксированный ответ: вызвали isUserFormOwner — получили {"isOwner": true}.
Фейки — работающие, но упрощённые реализации, как in-memory база вместо PostgreSQL. Моки проверяют факт вызова: метод X вызван дважды с аргументом Y.
Нам важен не факт вызова, а результат обработки — поэтому не моки. В текущей реализации мы используем заглушки: для каждого RPC-метода задаём JSON-ответ, и для наших сценариев этого достаточно. Но архитектура позволяет подставить реализацию любой сложности — от строки с JSON до полноценного фейка с логикой и состоянием.
Продуктовый сервис, который мы тестируем. В него добавляется специальный тестовый хендлер, не используемый в обычной работе.
Хендлер создает изолированное окружение: через middleware подменяет все внешние вызовы подготовленными ответами из тест-кейса.
mwFactory := autotestswrapper.ClientMiddlewareFactory(in.Interactions) storageClient := briefrpc.ClientFor[storage.Server](mwFactory.Option(storage.Name)) configClient := briefrpc.ClientFor[config.Server](mwFactory.Option(config.Name))
Затем инициализирует реальный продакшн-обработчик - тот же DI-граф, что в main.go, но с подменёнными клиентами.
storageAdapter := storage.New(storageClient) configAdapter := configurator.New(configClient) orderService := order.New(storageAdapter, configAdapter) handler := next_page.New(orderService)
Вызывает обработчик с данными из тест-кейса и сравнивает результат с ожидаемым.
err := handler.Handle(ctx, req, resp) if err := assertJSONEqual(in.Output, resp.Data); err != nil { out.Status = "failed" out.ErrorMessage = err.Error() } else { out.Status = "ok" }
Весь процесс занимает десятки миллисекунд. Никаких баз данных, внешних вызовов, тестовых пользователей — только код, изолированное окружение и подготовленные ответы от зависимостей.
Три компонента работают в связке: оркестрация генерирует тесты, координатор прогоняет, конечные сервисы выполняют проверки.
Оркестратор заточен под конкретный продукт. Ожидается, что каждая команда пишет свой генератор тест-кейсов — из конфигов, из спецификаций, из любого источника.
Процесс тестирования шаг за шагом
Пройдем путь от исходных данных до готового результата.
Шаг 1. Описание сценариев. В админке продукта хранятся конфиги для всех категорий услуг. Каждый конфиг — JSON с экранами, их типами, полями, вариантами выбора и правилами переходов. Это единственный источник правды о том, как должен работать продукт.
Шаг 2. Генерация тест-кейсов. Разработчик нажимает кнопку «Запустить автотесты». Система отправляет JSON в сервис оркестрации. Тот находит все возможные пути от точки входа до финального экрана, для каждого собирает последовательность шагов и определяет входные данные, ожидаемый ответ и фейки для зависимостей. На выходе — массив тест-кейсов, каждый с именем сервиса, именем хендлера, номером шага, входными данными, ожидаемым результатом и набором фейков.
Пример:
``` { "clientName": "my-service-api", "handlerName": "RunNextPageTests", "testCaseStep": 3, "testCase": { "input": "{\"requestType\":\"repair\",\"pageNumber\":1}", "output": "{\"success\":{\"params\":{\"PageTitle\":\"Где делаете ремонт\"}}}", "interactions": { "geo-service.getLocation": "{\"city\":\"Москва\"}", "storage-service.isFormOwner": "{\"isOwner\":true}" } } } ```
Шаг 3. Координация вызовов. Тест-кейсы попадают в сервис-координатор. Для каждого он смотрит, какой сервис и хендлер вызывать, использует рефлексию, чтобы найти нужный метод в рантайме, и вызывает его с входными данными.
Шаг 4. Изолированное выполнение в сервисе. Тестовый хендлер создаёт изолированный DI-граф с измененными клиентами, вызывает реальный продакшн-обработчик и сравнивает результат.
Шаг 5. Сбор результатов. Координатор собирает ответы от всех шагов. Результаты сохраняются в базу оркестратора — можно посмотреть историю прогонов, найти, на каком шаге и с каким конфигом произошла ошибка. Если хотя бы один шаг вернул ошибку, сценарий считается проваленным. В админке появляется уведомление: на каком шаге и в какой комбинации данных произошла ошибка. Если все успешно — конфиг готов к выкатке.
В реальности весь процесс занимает секунды. Для 100 категорий с 50 экранами каждая система генерирует и прогоняет несколько сотен тест-кейсов. Каждый выполняется за десятки миллисекунд.
Прогон не требует тестовых пользователей, БД или реальных вызовов соседних сервисов: нагрузка идёт только на CPU и память, что безопасно даже на проде.

Какие возможности дает система
При проектировании мы исходили из требований, которые классические инструменты не закрывали. В итоге система дала 4 возможности, оказавшиеся наиболее ценными:
Безопасный прогон
Гибкое фейкование
Дедупликация запросов
Покрытие автогенерируемого кода
Безопасный прогон на проде без создания тестовых пользователей. Обычные E2E-тесты требуют тестовых учеток: нужно создать, сгенерировать данные, следить, чтобы они не засветились в аналитике и бизнес-процессах. Учётные записи протухают, данные теряют актуальность, а иногда тестовые пользователи случайно выходят в прод.
В нашей системе тестовых пользователей нет. В тестовый хендлер передаётся всё необходимое для запроса: заголовки, параметры, контекст. Система воспринимает это как обычный запрос, но не создаёт сущностей в базах и не оставляет следов в бизнес-логике. Прогон можно запускать на любом окружении, включая прод, не боясь что-то сломать или засорить метрики. Метрики отключаются (metrics.WithEnabled(false)), логи помечаются тегом test: <название тега> для фильтрации. Прогон не оставляет следов в мониторинге.
Гибкое фейкование. Не всегда нужно подменять все зависимости. Иногда важна реальная интеграция с базой или конкретным сервисом. Система позволяет настраивать глубину фейкования для каждого теста.
Уровень подмены выбирается для каждой зависимости. Сейчас мы подменяем все внешние вызовы заглушками, но архитектура позволяет оставить часть зависимостей реальными или использовать фейки с логикой — middleware настраивается на уровне отдельного клиента.
Дедупликация запросов. Если в тысяче тестов нужно сходить в профиль и получить один и тот же ответ, классический подход сделает тысячу реальных вызовов. В нашей системе ответ описывается один раз в тест-кейсе и подставляется во все вызовы автоматически.
То же работает с любыми данными. В сервисе оркестрации мы формируем набор фейков, которые используются многократно. Координатор и тестовые хендлеры просто подставляют подготовленные данные. Вместо тысячи сетевых запросов — один раз подготовили, тысячу раз использовали.
Покрытие автогенерируемого кода. В микросервисах много кода генерируется автоматически: клиенты, модели, мапперы. Напрямую тестировать автогенерацию бессмысленно, но проверить, что сгенерированный код правильно используется в бизнес-логике, необходимо.
Система фейков прогоняет реальные хендлеры, которые активно используют автогенерируемые клиенты и модели. Если в новой версии клиента изменился контракт, а хендлер не адаптировали, тест упадет. Если маппер стал возвращать данные в другом формате, ошибка будет обнаружена. Мы косвенно покрываем автогенерируемый код, проверяя его использование в реальных сценариях.
Эти 4 возможности сделали систему полезной в повседневной разработке.
Что это дало на практике
Ручной регресс для одной категории услуг — это около 120 тест-кейсов на трёх платформах. При раскатке 2-4 фичей в спринт тестирование становилось узким горлышком. Система фейков автоматизировала ~30% этого регресса.
Экономия — около 2 часов на каждую раскатку только на проверке основного пользовательского сценария. За один спринт после внедрения мы сконфигурировали, протестировали и раскатили заявки в 16 новых категориях — раньше это заняло бы значительно больше времени.
Отдельная ценность — сдвиг проверки влево (shift left). Категорийный менеджер создаёт конфиг заявки в админке и тут же нажимает кнопку «Запустить автотесты». За секунды получает ответ: конфиг корректен или на каком шаге и экране возникла ошибка. Без привлечения QA, без ожидания в очереди на тестирование, без ручного прохождения сценариев.
Ограничения и место в пирамиде тестирования
Система фейков решает конкретную задачу: проверку сложных разветвленных сценариев на уровне продакшн-кода без поднятия инфраструктуры, но она не делает всего.
Система не заменит:
Юнит-тесты. Они проверяют отдельные функции и модули — самый быстрый и дешёвый уровень защиты. Наша система работает выше: со связками вызовов и бизнес-сценариями.
Интеграционные тесты. Когда важно проверить реальное взаимодействие с базой данных или внешним API, система фейков не подходит — она подменяет эти вызовы. Интеграционные тесты остаются в силе.
Проверку рендера на клиентах. Система проверяет бэкенд-логику, но не то, как данные отобразятся на экране, и не верстку. Для этого нужны компонентные тесты на клиентах, DSL-тесты, скриншотные тесты.
Классические E2E-тесты на полном стеке. Если нужно проверить работу системы с реальными базами и очередями, под нагрузкой — система фейков не даст ответа. Она изолирует зависимости для скорости и безопасности.
Что требуется для внедрения
Система не работает «из коробки». В каждом продуктовом сервисе, участвующем в сценариях, нужно добавить тестовый хендлер. Это копия инициализации из main.go с использованием библиотеки для изоляции и перехвата вызовов. При унифицированной структуре сервисов разработка занимает минут десять на один сервис. Тестовый хендлер на 95% повторяет инициализацию из main.go — это кандидат на кодогенерацию. При желании можно добавить шаг в пайплайн генерации, чтобы тестовый хэндлер со всей обвязкой создавался автоматически.
Также нужен источник данных для генерации тест-кейсов. У нас это были JSON-конфиги из админки. Если такого источника нет, его придется создать. Оркестратор привязан к домену. Для другого продукта нужно написать свой генератор тест-кейсов. Координатор и библиотека переиспользуются без изменений.
Место в пирамиде тестирования
Классическая пирамида: юниты в основании, выше интеграционные, на вершине UI и E2E. Система фейков не вписывается целиком ни в один уровень.
Она берет от юнитов скорость и изоляцию, от интеграционных — проверку взаимодействия внутри сервиса, от E2E — покрытие сквозных сценариев. Это гибридный слой между интеграционными и E2E-тестами: проверка бизнес-логики на уровне сервисов без поднятия инфраструктуры, но с реальным продакшн-кодом.
Есть возможность запускать тесты в CI из коробки
В текущей реализации запуск — из админки продукта. Интеграция в CI возможна: координатор и оркестратор — обычные RPC-сервисы, их можно вызывать из пайплайна.
Вопросы и ответы
Чем это лучше обычных API-тестов?
При обычном API-запросе инициализируется вся инфраструктура сервиса: базы, клиенты, тоглы, метрики. Запрос создает реальную нагрузку, спамит логи и может влиять на пользователей. При сотнях тестов нагрузка становится ощутимой.
В нашем подходе тестовый хендлер создает изолированный инстанс сервиса в памяти. Базы не поднимаются, клиенты инициализируются с подменёнными ответами, метрики отключаются. Код выполняется так же, как в продакшне. Это безопасно и не требует создания тестовых пользователей.
Кроме того, в нашем случае конфиги на стейдже отличаются от продакшена — стейдж не даёт гарантии, что продакшн-сценарий работает корректно.
Зачем запускать на проде, если зависимости подменены?
Ценность не в том, что мы тестируем реальные зависимости, а в том, что мы тестируем реальный продакшн-код: с актуальными тогглами, feature flags, конфигами. Конфиги на стейдже могут отличаться от прода. Мы проверяем не то, «работает ли сервис геолокации», а «правильно ли наш хендлер обрабатывает ответ геолокации в текущей продакшн-конфигурации».
Как быть с авторизацией и правами доступа?
В классическом E2E для авторизации создают тестового пользователя. В нашей системе мы передаём в тестовый хендлер все необходимое: токены, заголовки, контекст. Поскольку мы не ходим в реальные сервисы аутентификации, можем подставить любой нужный контекст — часть гибкого фейкования.
В тест-кейсе можно указать роль «администратор» или отсутствие авторизации. Фейк сервиса авторизации вернет соответствующий ответ. Так мы проверяем поведение хендлера в разных сценариях, не заводя реальных учётных записей.
Можно ли использовать без оркестратора?
Да. Координатор и библиотека самодостаточны. Тест-кейсы можно формировать вручную, из файлов, из CI-скрипта. Оркестратор — один из способов их генерировать, но не единственный.
Итоги
Все началось с того, что обычные E2E-тесты перестали работать. Сотни сценариев, множество сервисов, каждый новый конфиг — и непонятно, где возникнет ошибка. Поддерживать такой массив было невозможно.
Система фейков не решила все проблемы, но помогла перестать создавать тысячи тестов вручную. Мы научились генерировать их из описания бизнес-логики, запускать регресс на проде без тестовых пользователей и получать точную информацию о нарушениях после изменений.
Конечно, подход создан под нашу специфику: сложные сценарии, много данных, несколько сервисов в цепочке. Если у вас похожая боль — возможно, это пригодится. Но даже так система сэкономила время, уберегла от нескольких регрессов и дала уверенность.
Координатор и библиотека подмены универсальны. Они не знают о формах, экранах и категориях услуг. Любая команда может подключить свои сервисы и написать свой генератор тест-кейсов поверх готовой инфраструктуры.
Главный вывод для нас: не обязательно жестко делить тесты на «интеграционные» и «сквозные». Можно взять от каждого лучшее и сделать гибрид, который работает именно в вашем случае.
Если пробовали что‑то подобное или видите, как это можно улучшить — давайте обсудим в комментариях.

