Прокси — один из основных инструментов в арсенале QA-инженера. Charles Proxy, Fiddler и Proxyman давно стали стандартом для анализа и изменения сетевого трафика в процессе ручного тестирования. Их принцип работы хорошо известен и подробно описан во множестве материалов.
Однако возникает вопрос: как использовать подобные возможности в UI-автотестах? Как перехватывать или мокать трафик в автоматизированных сценариях? Давайте разберёмся.
1. Зачем нужен прокси в UI автотестах
Зачатую автотесты отлично себя чувствуют без дополнительных операций с трафиком, однако в некоторых ситуациях проксирование может сильно улучшить скорость и полноту проверок. Вот несколько причин по которым прокси будет незаменим.
1.1 Сетевая слепота автотестов
Слепость UI автотестов к ошибкам, неправильным ответам API, событиям WebSocket, неверным данным backend
На деле это может стать причиной/выглядеть как:
flaky tests
непонятные падения
невозможность воспроизвести состояния
ложным падениям из-за проблем сети или backend
сложностям при диагностике причин ошибки
Какие функций и проверки можно реализовать:
Перехват запросов и ответов для коротких api тестов и проверки поведения приложения
Перехват токена при регистрации с дальнейшим использованием во фреймворке
Троттлинг
Логирование трафика
Проверку валидности запросов клиента
Отслеживание последовательности запросов клиента
Проверка количества запросов
Перехват и анализ WebSocket сообщений
Проверка корректности обработки ошибок
Таким образом доступ из автотеста к информации о работе сети добавляет им важное качество - network visibility. После падения автотеста уже на этапе отчета мы можем сузить круг возможных причин исключив сетевые проблемы или наоборот списать все на сеть)
1.2 Сложности с получением тестовых состояний
Незаменимая функция при тестировании поведения клиентских приложений когда получить тестовое состояние через DEV стенд долго/дорого и когда нет задачи тестировать бекенд!
Варианты использования:
Подмена статус кодов (200 → 4xx, 5xx для проверки поведения клиента)
Изменение параметров json/xml в запросах и ответах
Изменение параметров хедеров запросов и ответов
Подмена тела ответа/запроса целиком
Инжектирование WS сообщений в канал (изменение баланса юзера, отправка нотификаций и другое)
Изменение WS сообщений на лету (изменение параметров в json внутри WS)
Другие специализированные функции
То есть во многих случаях мы можем уйти от использования DEV стендов для симуляции некоторых событий или дополнить набор тестов быстрыми проверками, что в значительной степени улучшает полноту и скорость тестирования программного продукта.
Именно получение тестовых состояний с помощью прокси оказалось киллер-фичей на нашем проекте с десятками дев стендов и ни одном, где можно быстро и просто настроить необходимое состояние и уж тем более интегрировать его в UI автотесты клиентских приложений.
3. Поиск решения
Charles / Proxyman / Fiddler не подходят для автоматизации по понятным причинам - в них нет функционала управления извне кроме GUI. После долгих ресерчей остановились на mitmproxy, ****который изначально проектировался как программируемый прокси с командной строкой и веб интерфейсом

Python API — позволяет писать скрипты, которые перехватывают и модифицируют запросы и ответы.
Headless-режим — прокси легко запускать в CI как без графического интерфейса так и с ним
Гибкое управление трафиком — можно логировать, изменять или мокать ответы сервера.
Поддержка WebSocket — важная возможность для современных мобильных приложений.
Мультиплатформенный open-source продукт - доступен для Windows, Linux, MacOs
По сути, mitmproxy превращается из инструмента анализа трафика в управляемый сетевой слой, который можно встроить прямо в инфраструктуру тестов.
4. Архитектура решения
После нехитрой установки нашей проксИ https://docs.mitmproxy.org/stable/overview/installation/
brew install mitmproxy
И получении сертификатов на устройства https://docs.mitmproxy.org/stable/concepts/certificates/
Мы получаем стандартный прокси-флоу:

Работа прокси настроена в режиме аналогичному для упомянутых Charles / Proxyman / Fiddler с тем отличием, что mitm можно запустить в 3х режимах:
Mitmproxy предоставляет вам интерактивный интерфейс командной строки
Mitmweb предоставляет вам графический интерфейс на основе браузера
Mitmdump дает вам неинтерактивный терминальный выход
https://docs.mitmproxy.org/stable/overview/getting-started/
5. Настройка mitmproxy для автотестов
Первое что нужно понять, что запуск mitmproxy осуществляется не тестовым фреймворком, а отдельным процессом и сделать это можнo внутри экземпляра терминала в вашем IDE(я использую PyCharm на MacOs) или в отдельном окне терминала ОС.

И чтобы воспользоваться Python API и интегрировать прокси в автотесты необходимо создать модуль proxy_handler.py и реализовать в нем методы которые позволят работать с HTTP запросами и WS (код модуля HTTP разберем ниже)
Запуск прокси происходит через команд-лайн следующим образом или через bash-скрипт (можно посмотреть в репо):
mitmweb -s proxy_handler.py
В качестве механизма взаимодействия между тестовым фреймворком и прокси выступает config.json. Именно он позволяет запускать и останавливать все те функции которые мы будем реализовывать для прокси.
6. Реализация подмены статус кода
Давайте рассмотрим пример реализации подмены СК для конкретного API. Это наиболее простая операция с точки зрения реализации кода.
6.1 Config.json
Предназначен для настройки прокси на выполнение той или иной операции.
В файле config.json создаем пару ключ-значение:
{ "status":{} }
В значении оставим пустой объект как признак того что ничего изменять не нужно.
Для инициации подмены СК вместо пустого объекта запишем:
{ "status": {"api/v1/user": 404, "api/v2/settings": 500} }
Парсинг нужных API реализован с применением модуля RE поэтому прокси найдет любые вхождения в URI, включая query-параметры, что очень удобно и значительно расширяет возможности. Подмена в таком формате записи совершится 1 раз для каждого ключа.
Для продолжительной подмены нужно использовать форму записи:
{ "status": {"/api/v1/user": [404], "api/v2/settings": [500]} }
Где СК является 0 элементом списка, что логически бессмысленно, но в таком формате подмена будет совершаться до конца проверки или текущего тест-кейса. Такой формат был выбран для исключения ввода еще одного параметра в config.json и удачно себя зарекомендовал. Отключение подмены возможно со стороны фреймворка изменением значения “status” на {} или на другие параметры.
6.2 Модуль Proxy_handler.py
К самому интересному - в данном файле будем писать методы которые будут читать config.json, выполнять подмены CК и изменять config.json чтобы остановить подмены. Сделаем упор на производительность, чтобы не тормозить работу прокси ненужными проверками или кривым кодом, в меру своих возможностей конечно)
import re import mitmproxy.ctx as ctx from mitmproxy import http from file_worker import FileWorker file_worker = FileWorker() def response(flow: http.HTTPFlow) -> None: url = flow.request.url if not flow.response.content: return cfg = file_worker.get_proxy_params() "STATUS CODE override" cfg_status = cfg.get("status") for api, sc in list(cfg_status.items()): "Compare 'api' with URI (compiling with 're')" if bool(re.compile(api).search(url)): "Changing the STATUS CODE" flow.response.status_code = int(sc[0] if isinstance(sc, list) else sc) ctx.log.info(f"Status code was mocked '{api}' -> {sc}") if not isinstance(sc, list): del cfg_status[api] "Updating a 'config.json'" file_worker.set_proxy_param("status", cfg_status) cfg["status"] = cfg_status break
В коде есть короткие пояснения про то как происходит обработка config.json и подмена СК. Вот основные тезисы на которые стоит обратить внимание:
Начинаем подмену только когда в поле “status” is not {}
for api, sc in list(cfg_status.items()):Используя пакет RE сравниваем URI и ключи внутри "status": {"api/v1/user": … Так, можно использовать регулярные выражения для тонкой настройки API для подмены, например условия с END - “(?=.*api/v3/banner/list)(?=.*position=promotion)” или NOT - ”^(?!config\.examplepis\.com(?::\d+)?$)(?:.+\.)?examplepis\.com(?::\d+)?$”
if bool(re.compile(api).search(url)):Заменяем статус код обрабатывая оба варианты с разовой заменой и бесконечной
flow.response.status_code = int(sc[0] if isinstance(sc, list) else sc)Пишем лог в консоль и прекращаем подмену если необходимо и прерываем цикл для данного запроса
if not isinstance(sc, list): del cfg_status[api] "Updating a 'config.json'" file_worker.set_proxy_param("status", cfg_status) cfg["status"] = cfg_status
Флоу работы с файлами вынесен в отдельный модуль - скорее всего в вашем фреймворке будет реализация этих функций и вы можете взять готовые методы. Вот мой пример:
class FileWorker: def __init__(self): self.current_directory = os.path.abspath(os.path.join(os.environ["VIRTUAL_ENV"], "..")) self.__proxy_proxy_data = os.path.join(self.current_directory, "config.json") @staticmethod def __file_json_to_dict(file_path: str) -> dict: with open(file_path, encoding='utf-8-sig') as filestream: text_from_file = filestream.read() temp_dict = json.loads(text_from_file) return temp_dict def get_proxy_params(self) -> dict: return self.__file_json_to_dict(self.__proxy_proxy_data)
Далее по аналогии можно реализовать подмену (Mocking) параметров в ответе:
"MOCK params" cfg_mock: dict = cfg.get("mock") if cfg_mock: for mock_api, params in list(cfg_mock.items()): if bool(re.compile(mock_api).search(url)): data = flow.response.content.decode() modified = file_worker.mock(params[0] if isinstance(params, list) else params, data) if modified: flow.response.content = modified.encode() ctx.log.info(f"Param {params} were mocked for '{mock_api}'") else: ctx.log.info(f"No matches found for {params}") if not isinstance(params, list): del cfg_mock[mock_api] file_worker.set_proxy_param("mock", cfg_mock) cfg["mock"] = cfg_mock break
Или сохранение ответа сервера в файл для дальнейшего разбора в ходе проверок:
"SAVE RESPONSE to file" cfg_response: list = cfg.get("get_response") if cfg_response: for i, resp_api in enumerate(list(cfg_response)): if bool(re.compile(resp_api[0] if isinstance(resp_api, list) else resp_api).search(url)): ctx.log.info(f"Response '{url}' was saved with #{i}") file_worker.set_proxy_temp_file(f"response_{i}.json", flow.response.get_text()) if not isinstance(resp_api, list): cfg_response.pop(i) file_worker.set_proxy_param("get_response", cfg_response) cfg["get_response"] = resp_api break
7. Применение в тестовом фреймворке
Mitmproxy и тот addon который мы реализовали довольно просто встроить в тестовый фреймворк. Достаточно реализовать метод который будет менять config.json и работать с полученными файлами.
В итоге в логе прокси мы видим короткий отчет о проделанных подменах.

Здесь реализованы подмена статус-кода, параметров в ответе в формате json и сохранение json в файл для дальнейшей работы с ним (например, ассерты UI согласно полученному ответу).
В этой статье я рассмотрел только базовые функции и настроен написать еще немного про подмену данных Websocket и прочие тонкие настройки прокси.
Кому зашло, ставьте лайк, продолжим разборы более сложных материй :-)
Всем добра, и не забывайте тестить бекенд :-)
