E2E (end-to-end) тестирование мобильных приложений часто становится сложной задачей из-за большого числа переменных.
Дело в том, что мобильное приложение имеет как фронтенд (то, с чем взаимодействует пользователь), так и бэкенд (где обрабатываются данные). И одна из самых распространённых и сложных задач при тестировании – выяснить, связана проблема с фронтендом или с бэкендом.
На практике при сквозном тестировании мобильного приложения поиск причины сбоя может занимать значительное время. Часто он требует участия нескольких команд, каждая из которых отвечает за свою часть системы. Тестировщикам приходится координировать их работу, чтобы выявить источник проблемы.
Друзья, поддержите нас вступлением в наш телеграм канал QaRocks. Там много туториалов, задач по автоматизации и книг по QA.
Как упростить отладку?
Искать места возникновения ошибок сожно, и это замедляет процесс тестирования. Но что, если исключить одну из переменных?
Представьте, что для мобильного приложения можно создать полностью функциональный E2E-процесс, не зависящий от бэкенда. Это исключит внешние зависимости, ускорит тестирование и упростит отладку.
В результате мы получим что-то типа расширенного компонентного тестирования, где внимание сосредоточено только на мобильной части, без учёта взаимодействия с бэкендом.
Из-за сложности отслеживания проблем между фронтендом и бэкендом требуется более управляемый подход к тестированию. Здесь на помощь приходят сервисы-заглушки.
Сервисы-заглушки
Обычно в архитектуре системы мобильное приложение взаимодействует с сервером, который обрабатывает данные и возвращает ответы. На Изображении 1 показан этот процесс с несколькими уровнями взаимодействия.

Подмена бэкенда
Можно ли убрать весь бэкенд и при этом сохранить полноценное выполнение всех операций? Да, и решение — это использование сервисов-заглушек (англ. stub service, “стаб-сервис”) или, проще говоря, имитация работы бэкенда с помощью предопределенных ответов, как показано на Изображении 2.

Роль сервиса-заглушки
На Изображении 2 показано, как сервис-заглушка внедряется между мобильным приложением и бэкендом. Он заменяет бэкенд, имитируя его поведение, и возвращает приложению заранее подготовленные ответы.
Стаб-сервис перехватывает и обрабатывает все запросы, позволяя тестировать приложение, как будто бэкенд полностью функционирует.
В чём главное преимущество стаб-сервиса? Приложение работает так, будто взаимодействует с настоящим бэкендом, хотя фактически взаимодействует с его имитацией. Это обеспечивает удобное тестирование, при котором приложение “не замечает”, что использует заглушку.
Теперь, когда мы познакомились с понятием сервисов-заглушек, давайте разберём, как они работают и как их настроить для эффективного тестирования мобильных приложений.
Как это работает?
Стаб настраивается так, чтобы выдавать определённый ответ на ожидаемый запрос от приложения. Для этого требуется задать три ключевых элемента:
- Детали запроса. Какие запросы ожидаются, включая HTTP-метод и URL.
- Параметры. Сюда входят все необходимые параметры запроса или основная информация.
- Ответ. Желаемый ответ, который должен вернуть сервис при совпадении с заданным запросом.
Пример. Представим, что приложение отправляет запрос:
GET /books/find/123
В этом случае стаб настроен на возвращение заранее определённого ответа:
{ "title": "automation best practices", "year": "2024" }
Процесс обработки запросов
Поскольку мы уже знаем, как должно вести себя приложение, мы можем заранее предсказать запрос. Когда сервис-заглушка получает запрос, он выполняет следующие шаги:
- Сопоставление запроса. Сервис проверяет URL, метод и параметры запроса, сравнивая их с заранее настроенными.
- Возврат ответа. Если запрос совпадает с конфигурацией, возвращается предопределённый ответ. Если совпадения нет, сервис выдает ошибку, сообщая, что соответствующий стаб не найден.
Хотя эти сервисы могут показаться сложными, на самом деле, если вся инфраструктура настроена, их реализация проста.
Давайте разберёмся поэтапно
Создание инфраструктуры для стаб-сервисов требует времени и усилий. Однако, когда она будет готова, их настройка станет быстрой и понятной.
Как же это сделать?
Первый шаг — добиться того, чтобы приложение взаимодействовало только с сервисом-заглушкой. Этого можно достичь, настроив специальный флейвор (англ. flavor) приложения.
Флейвор приложения
Во флейворе приложения все URL-адреса, используемые приложением, заменяются адресами стаб-сервиса. Это дает нам, тестировщикам, полный контроль над коммуникацией приложения.
После того как приложение настроено, мы переходим к настройке надежного стаб-сервиса, который будет обрабатывать входящие запросы и возвращать определенные заранее ответы.
Умный стаб-сервис
Сервис-заглушка должен быть способен обрабатывать всю входящую коммуникацию от приложения. Для этого нужны:
- Конечная точка для приёма любых входящих запросов, будь то REST или GraphQL.
- Конечные точки для сохранения и настройки предопределенных ответов.
- Функционал для отслеживания каждого исходящего запроса от приложения.
На Изображении 3 подробно показано, как это работает.

Теперь давайте рассмотрим основные компоненты, необходимые для полноценного функционирования стаб-сервиса. И первым делом мы должны убедиться, что наша заглушка может обрабатывать сообщения от приложения.
Конечная точка для приёма входящих запросов
Как может стаб-сервис принимать все входящие запросы? Всё просто: с помощью одной конечной точки, которая может обработать любой тип запроса.
Ниже приведён пример REST-эндпоинта для обработки запросов (на языке Kotlin):
@RequestMapping(value = "/stub", method = [RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE]) fun handleRequest( @RequestBody(required = false) body: String?, request: HttpServletRequest ): ResponseEntity<String> { // Log the request method and URL println("Received ${request.method} request to ${request.requestURI}") // Optionally, you can log headers or other parts of the request println("Headers: ${request.headerNames.toList()}") println("Body: $body") // Here we would check against the configured stubs and return a response val response = matchStubbedResponse(request, body) return if (response != null) { ResponseEntity.ok(response) } else { ResponseEntity.status(HttpStatus.NOT_FOUND).body("No matching stub found") } }
После обработки входящей коммуникации нужно объяснить стаб-сервису, как он должен себя вести при каждом запросе.
Конечные точки для сохранения и настройки предопределенных ответов
Для определения наших заглушек нам понадобится конечная точка, которая позволит нам создавать и сохранять в базе данных конфигурации стабов с уникальными идентификаторами для отслеживания и отладки.
Ниже приведён пример упрощённой конечной точки для создания стабов:
@RestController class StubController(val stubService: StubService) { @PostMapping("/stub/create") fun createStub(@RequestBody stubRequest: StubRequest): ResponseEntity<String> { // Save the stub to the database with a unique ID val stubId = stubService.saveStub(stubRequest) return ResponseEntity.ok("Stub created with ID: $stubId") } } data class StubRequest( val method: String, val url: String, val requestBody: String?, val responseBody: String, val responseStatus: Int )
При управлении стабами важно учитывать их жизненный цикл. Хотим ли мы, чтобы каждый раз возвращался один и тот же ответ, или он должен меняться в зависимости от разных состояний? Например, одна и та же конечная точка может сперва вернуть один ответ, а затем, когда состояние приложения изменится, — другой. Хотя такая гибкость важна для сложных тестов, пока что мы для простоты сосредоточимся на статичных ответах-заглушках.
Функционал для отслеживания запросов приложения
Отслеживание и сопоставление всех исходящих от приложения запросов требует тщательного планирования и тесного сотрудничества с командой разработки. Важно точно знать, какие запросы делает приложение, и какие метаданные связаны с каждым из них.
Например, когда пользователь входит в приложение, оно генерирует уникальный ID сессии (который меняется при каждом входе). Нам нужно, чтобы каждый последующий запрос от приложения содержал этот ID сессии в отдельном заголовке.
Что это даёт?
- Мы можем сопоставить стаб не только с конкретным запросом, но и с конкретным вызовом, сделанным в определённой тестовой сессии.
- Мы можем запускать несколько тестов параллельно, зная, что стабы не будут мешать друг другу, так как у каждого теста будет свой уникальный ID сессии.
- Мы можем выбрать, должен ли стаб быть универсальным (возвращать одинаковый ответ всегда) или специфичным (возвращать ответ только для этого конкретного ID сессии).
На Изображении 4 показано, как настроен стаб-сервис для обработки конкретного запроса, сделанного в конкретном тесте.

Вот обновлённый фрагмент кода, который учитывает заголовок с ID сессии при сопоставлении стабов:
@RequestMapping(value = "/stub", method = [RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE]) fun handleRequest( @RequestBody(required = false) body: String?, request: HttpServletRequest ): ResponseEntity<String> { val sessionId = request.getHeader("Session-Id") println("Received ${request.method} request to ${request.requestURI} with Session-Id: $sessionId") val response = matchStubbedResponse(request, body, sessionId) return if (response != null) { ResponseEntity.ok(response) } else { ResponseEntity.status(HttpStatus.NOT_FOUND).body("No matching stub found") } }
Итак, подытожим
Наше решение направлено на упрощение тестирования мобильных приложений за счет исключения необходимости в реальном бэкенде. Мы достигаем этого с помощью введения стаб-сервиса. Он имитирует работу бэкенда и обрабатывает все запросы приложения, отсылая предопределенные ответы на них. Давайте повторим, как это работает.
Взаимодействие с приложениями
Мы настраиваем приложение так, чтобы оно общалось исключительно с нашим стаб-сервисом. Для этого заменяем реальные URL бэкенда на адреса стаб-сервиса с помощью специального флейвора приложения.
Обработка запросов
Стаб-сервис обрабатывает все входящие запросы, пытается сопоставить их с заранее заданными конфигурациями и возвращает соответствующие ответы-заглушки. Если совпадение не найдено, возвращается ошибка.
Точное сопоставление запросов
Мы добавляем ID сессии в запросы, чтобы заглушки были привязаны к конкретным тестам. Это позволяет запускать тесты параллельно и гарантирует, что каждый стаб будет использоваться только в своей тестовой сессии.
Гибкость стабов
Стабы могут быть как универсальными (всегда возвращающими один и тот же ответ), так и специфическими (предназначенными для определённых ID сессий), что даёт гибкость в зависимости от сценария тестирования.
Контролируя клммуникацию приложения, можно исключить бэкенд как переменную при тестировании. Это позволяет сосредоточиться только на поведении мобильного приложения.
Планирование стабов с помощью таких инструментов, как BrowserStack
Для эффективного планирования и настройки стабов можно использовать специальные инструменты. Например, BrowserStack предлагает инструмент для отслеживания сетевых запросов. Этот инструмент захватывает и отображает все запросы, сделанные приложением, включая их детали, данные ответов и порядок запросов.
Используя этот инструмент, мы можем:
- Легко наблюдать весь сетевой поток приложения, понимая, какие запросы были сделаны и когда
- Использовать полученную информацию для точной настройки конфигураций стабов, чтобы они точно отражали поведение приложения
- Отслеживать порядок вызовов для создания более точных заглушек для различных этапов работы приложения.
Инструменты, подобные BrowserStack, незаменимы при планировании и оптимизации тестирования с использованием заглушек.
Заключение
Рассмотренное в этой статье решение позволяет быстро и эффективно получать ценные данные о состоянии приложения — без тайм-аутов, конфликтов версий или блокировок среды.
То, что мы рассмотрели, — это только начало. Внедрение подхода с использованием стабов потребует продуманного планирования и исполнения, но, как только всё будет настроено, и тестировщики, и разработчики получат мощный инструмент.
Этот подход позволит вам с уверенностью определять корни проблем, упрощая процесс тестирования и повышая общую эффективность.
Перевод статьи «The Complexity of Mobile E2E Testing: A Common Dilemma».