← Блог

E2E без полного окружения: что можно подменить и где тест теряет смысл

E2E без полного окружения: что можно подменить и где тест теряет смысл

Коротко. Зависимость можно заменить, пока реальный код выполняет проверяемое действие, а двойник сохраняет условия, от которых зависит результат. Подмена лишает тест смысла, если сама создаёт ожидаемый результат, скрывает значимую ошибку запроса или исключает проверяемый механизм.

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

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

Как определить границы проверки

Границу проще провести, если закончить фразу: «Этот тест подтверждает, что…»

«После ответа об успешной оплате интерфейс показывает подтверждение» и «после оплаты заказ сохраняется и передаётся на отгрузку» — разные утверждения. Для первого достаточно управлять ответом API. Для второго нужно выполнить серверную обработку и проверить её последствия. Совместимость с настоящим платёжным провайдером — ещё одно самостоятельное утверждение.

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

Границу стоит отразить в названии и описании теста: «оформление заказа с имитацией API оплаты» точнее, чем «оплата работает».

Термины тоже обозначают разные виды подмены. Stub возвращает подготовленные ответы, fake содержит упрощённую работающую реализацию, mock проверяет ожидаемые взаимодействия. Дальше «двойник» — общее название подмены. Его пригодность определяется сохранённым поведением, а не названием библиотеки.

Изоляция без замены поведения

Изолировать тесты друг от друга и заменить зависимость — разные решения. Отдельные пользователи и заказы изолируют данные этих сущностей; общие остатки и лимиты остаются разделяемыми и требуют другого подхода. Отдельный экземпляр PostgreSQL снимает зависимость от общей базы, сохраняя поведение выбранной СУБД.

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

Если нестабильность создаёт общий ресурс, отдельный экземпляр может решить проблему без упрощения поведения зависимости.

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

Рассмотрим учебный магазин. Заказ стоит 1 990 рублей; окончательное подтверждение оплаты приходит через вебхук (webhook) — HTTP-уведомление провайдера. Названия запросов и статусов ниже условные.

Для проверки обработки заказа настоящими остаются браузер, серверная часть, платёжный адаптер, обработчик вебхука и тестовая БД. Внешний платёжный API заменяется HTTP-сервисом с управляемыми ответами. Рабочий адаптер обращается к нему по тестовому адресу.

Сценарий состоит из пяти действий:

  1. Пользователь оформляет заказ. Серверная часть сохраняет его в состоянии «ожидает оплаты».
  2. Платёжный адаптер отправляет запрос двойнику и получает идентификатор платежа со статусом «обрабатывается».
  3. Тест доставляет на настоящий эндпоинт вебхука уведомление об оплате с идентификатором полученного платежа.
  4. Обработчик проверяет уведомление, сохраняет новый статус заказа и создаёт задание на отгрузку.
  5. Интерфейс получает актуальный статус через настоящий API.

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

Пользователя или корзину можно подготовить напрямую, если их создание находится вне границы сценария. Но записать в БД статус «оплачен» после отправки уведомления — значит выполнить работу проверяемого обработчика вместо него. Подготовка исходного состояния не должна создавать ожидаемый результат.

На том же сценарии видно, как точка подмены меняет охват:

Где подменить оплату, чтобы проверить заказ
Где стоит двойникЧто выполняется на самом делеЧто остаётся за границей
В браузере, вместо ответа собственного /checkoutКлиентская обработка подготовленного ответаСерверное оформление заказа для этого запроса
Вместо метода платёжного адаптераЛогика приложения до и после вызова методаФормирование платёжного HTTP-запроса, транспорт и разбор ответа адаптером
На месте внешнего платёжного HTTP APIСерверная часть, рабочий адаптер, HTTP-клиент и обработка смоделированного ответаРеальное поведение, доступность и конфигурация провайдера

В Playwright перехват через route.fulfill возвращает подготовленный ответ без обращения к соответствующему API. Перехват браузерного запроса не управляет отдельным соединением серверной части с платёжным сервисом: для него нужен двойник на серверной границе.

Другой режим, route.fetch, сначала выполняет настоящий запрос, после чего позволяет изменить ответ. К этому моменту сервер уже мог создать заказ или платёж. Ответ, исправленный для браузера, не отменяет эти последствия и не устраняет зависимость от доступности сервера.

Почему правильного ответа недостаточно

В учебном API сумма передаётся в копейках. Для заказа на 1 990 рублей приложение должно отправить amount: 199000, но из-за ошибки адаптера отправляет amount: 1990.

Оба значения соответствуют схеме «целое число». Провайдер тоже может принять меньшую сумму: он не обязан знать стоимость корзины магазина. Заглушка с безусловным успехом на любой POST /payments такой дефект пропустит.

Поэтому тест проверяет сумму, валюту, связь платежа с заказом и нужный ключ идемпотентности. Ожидаемые значения формируются из условий заказа, независимо от ответа двойника. Нельзя вычислять их тем же кодом, который создаёт платёжный запрос.

Строгость здесь означает точность только по значимым полям. Жёстко заданный генерируемый ID или порядок полей JSON добавят хрупкость. Связь полученного идентификатора платежа с заказом, напротив, влияет на результат и должна проверяться.

Успех сценария подтверждается сохранённым заказом и заданием на отгрузку — через API, который читает фактическое состояние, или напрямую в хранилище. Сообщения «Оплата прошла» на странице недостаточно. При асинхронной обработке тест ожидает конкретное состояние с ограниченным сроком, а не просто делает паузу.

Какие зависимости можно изолировать

Универсального списка «свои сервисы оставляем, внешние заменяем» нет. Собственный сервис может быть несущественным для выбранного утверждения, а внешний — составлять предмет проверки.

Какие зависимости можно изолировать
ЗависимостьКогда подмена уместнаКогда нужна настоящая реализация
Платёжный APIПроверяется реакция приложения на отказ, задержку или заданный статусПроверяется совместимость адаптера с API провайдера, его авторизацией и уведомлениями
Почта и SMSВажны факт отправки, адресат, содержимое и ссылка; сообщение перехватывается на выходеТребуется подтвердить работу с провайдером или доставку в почтовый ящик либо на телефон
Сервис соседней командыПроверяется поведение потребителя при оговорённых ответах, а ожидания сверяются с поставщикомОшибка может находиться в реальной последовательности взаимодействий нескольких сервисов
База данныхПроверяется логика, не зависящая от SQL, транзакций и ограничений конкретной СУБДУтверждение касается сохранения, миграций, конкурентных изменений или транзакционной целостности
Брокер сообщенийПроверяется реакция обработчика на содержимое уже полученного сообщенияВажны публикация, маршрутизация, подтверждение обработки, повторная доставка или восстановление соединения
АналитикаЕё обработка вне цели сценария; отправка и состав событий при необходимости проверяются тестовым приёмникомТребуется подтвердить приём, обработку или использование события настоящей аналитической системой

Для хранения важна разница между движками. SQLite остаётся настоящей СУБД, но не воспроизводит все особенности PostgreSQL. Так же и очередь в памяти не сохраняет семантику брокера: подтверждение публикации и подтверждение обработки потребителем относятся к разным участкам доставки.

Две менее заметные зависимости — аутентификация и время. Сохранённая сессия позволяет начать сценарий уже вошедшим пользователем, но не проверяет сам вход через провайдера идентификации. Отключение проверки токена меняет охват ещё сильнее, потому что приложение перестаёт проверять права на реальные действия.

Управление временем в браузерном контексте тоже не влияет автоматически на серверную часть. Если срок заказа проверяется на сервере, перевод часов страницы не подтверждает истечение срока. Контролировать время нужно там, где принимается решение, либо через подходящие данные и ожидание реального условия.

Какие сбои должен воспроизводить двойник

Управляемый двойник позволяет воспроизвести отказ, который трудно вызвать на общем стенде. Но для этого нужно задать конкретный исход запроса:

  • Пришёл HTTP-ответ со статусом 500. Клиент получил полноценный ответ и обрабатывает ошибочный статус.
  • Соединение оборвалось. Клиент не получил полноценного ответа; обработка идёт по ветке транспортной ошибки.
  • Истёк срок ожидания. Клиент прекратил ждать, но сервер мог выполнить операцию.
  • Пришёл ответ с повреждённым телом. Ошибка возникает при чтении или разборе данных.

Эти ситуации не взаимозаменяемы. Например, по стандарту Fetch сетевой отказ до получения заголовков отклоняет Promise, а HTTP-ответ с ошибочным статусом сам по себе этого не делает. Заглушка с кодом 500 не проверяет восстановление после обрыва связи.

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

Повторное событие после сохранения результата

Вернёмся к заказу. В условиях примера обработчик вебхука в одной транзакции сохраняет статус «оплачен», отметку об обработанном событии и задание на отгрузку.

Тест воспроизводит конкретную последовательность: транзакция зафиксирована, успешный HTTP-ответ не доходит до отправителя, приложение перезапускается с той же БД, событие приходит повторно. Ожидаемый результат — один оплаченный заказ и одно задание на отгрузку.

Произвольный тайм-аут не доказывает, что данные успели сохраниться. Точка сбоя должна быть управляемой и подтверждённой. Перезапуск здесь существенен: он показывает, не хранилась ли защита от повторов только в памяти процесса.

Одного статуса «оплачен» недостаточно: повторное присваивание не изменит его, даже если вторая отгрузка уже создана. Поэтому тест проверяет, что задание на отгрузку осталось одно. Сетевых обращений при этом может быть больше одного.

Документация Stripe по вебхукам предупреждает о повторных событиях и отсутствии гарантированного порядка доставки. Для выбранного провайдера нужно отдельно установить правила повторов и идентификации событий. Последовательный повтор из примера не проверяет гонку двух одновременных обработчиков — для неё требуется отдельный сценарий.

Как проверить сам fake

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

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

Для сопровождения двойника нужны понятные правила:

  • Непредусмотренный запрос завершает тест ошибкой, а не получает универсальный успешный ответ.
  • Изменяемое состояние не остаётся общим для параллельных тестов и сбрасывается перед независимым сценарием.
  • Для двойника известны владелец, версия поддерживаемого интерфейса и поводы для сверки: изменение SDK, контракта, адаптера или поведения провайдера.
  • Рядом с реализацией перечислены ограничения: какие ответы, переходы и отказы она умеет воспроизводить.

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

Готовый двойник от поставщика не освобождает от этой проверки. Связь с поставщиком сама по себе не гарантирует поддержку нужного состояния и последовательности операций.

Чем дополнить проверку с подменёнными зависимостями

Для каждого значимого риска, исключённого подменой, нужна отдельная проверка. Иначе этот риск следует явно оставить неподтверждённым.

Контракт подтверждает договорённость на границе

В проверках контрактов, которыми управляет потребитель (consumer-driven), например Pact, потребитель работает с имитацией провайдера, а записанные ожидания затем проверяются на стороне настоящего поставщика. Контракт помогает контролировать предположения, на которых построена подмена.

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

Когда внешний провайдер не проверяет потребительские контракты, используемые взаимодействия приходится сверять с документацией и доступным тестовым API. Спецификация OpenAPI помогает проверить описанный интерфейс, но не заменяет проверку поведения за его границами.

Какие проверки оставить на настоящих компонентах

Для учебного магазина набор может выглядеть так:

  • Адаптер и тестовое API провайдера: реальные запросы, авторизация, используемые ответы, совместимость уведомлений.
  • Приложение и рабочий движок БД: миграции, ограничения, транзакции, сохранение защиты от повторов.
  • Производитель, брокер и потребитель: публикация, маршрутизация и повторная доставка — если они участвуют в процессе.
  • Развёрнутое приложение: критический путь через конфигурацию, маршруты и права доступа, которых нет в локальной сборке.

Размер этого набора определяется исключёнными рисками. Повторять на полном стенде каждую комбинацию отказов необязательно. Но и один успешный smoke-тест не подтверждает все свойства реальной интеграции.

Совместимость отдельных пар сервисов ещё не подтверждает весь процесс: взаимодействия могут быть корректны по отдельности и нарушать общий порядок действий. Такой риск проверяется на совместно работающем подмножестве сервисов.

Изменение платёжного адаптера или версии SDK — повод проверить соответствующую интеграцию до выпуска. Периодические запуски помогают обнаружить изменения внешней стороны между релизами.

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

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

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

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

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

Что сохранять вместе с результатами

В результатах стоит фиксировать проверяемую границу и существенный состав окружения: версии приложения и двойника, режим платёжной зависимости, движок БД, версию контракта. Полезна и ссылка на проверку той части интеграции, которая исключена из этого сценария.

Например, команда может договориться о параметрах payment=fake, payment=sandbox, database=postgres. Это собственные обозначения команды, а не стандартные поля инструментов.

В ТестОпс сведения об окружении связываются с результатами через настроенное сопоставление переменных; данные передаются интеграцией с CI или allurectl. Саму зависимость переключает тестовая инфраструктура, а TMS хранит контекст, необходимый для интерпретации запуска.

При сравнении тестовых запусков одинаковый статус при payment=fake и payment=sandbox читается с учётом этих условий. Первый результат относится к реакции на заданную модель, второй — к взаимодействию с тестовым сервисом.

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