Прокси — один из основных инструментов в арсенале 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 и прочие тонкие настройки прокси.
Кому зашло, ставьте лайк, продолжим разборы более сложных материй :-)
Всем добра, и не забывайте тестить бекенд :-)
Автор: kazeboba
