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

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

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

И чтобы воспользоваться Python API и интегрировать прокси в автотесты необходимо создать модуль proxy_handler.py и реализовать в нем методы которые позволят работать с HTTP запросами и WS (код модуля HTTP разберем ниже)
Запуск прокси происходит через команд-лайн следующим образом или через bash-скрипт (можно посмотреть в репо):
mitmweb -s proxy_handler.py
В качестве механизма взаимодействия между тестовым фреймворком и прокси выступает config.json. Именно он позволяет запускать и останавливать все те функции которые мы будем реализовывать для прокси.
Давайте рассмотрим пример реализации подмены СК для конкретного API. Это наиболее простая операция с точки зрения реализации кода.
Предназначен для настройки прокси на выполнение той или иной операции.
В файле 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” на {} или на другие параметры.
К самому интересному - в данном файле будем писать методы которые будут читать 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
Mitmproxy и тот addon который мы реализовали довольно просто встроить в тестовый фреймворк. Достаточно реализовать метод который будет менять config.json и работать с полученными файлами.
В итоге в логе прокси мы видим короткий отчет о проделанных подменах.

Здесь реализованы подмена статус-кода, параметров в ответе в формате json и сохранение json в файл для дальнейшей работы с ним (например, ассерты UI согласно полученному ответу).
В этой статье я рассмотрел только базовые функции и настроен написать еще немного про подмену данных Websocket и прочие тонкие настройки прокси.
Кому зашло, ставьте лайк, продолжим разборы более сложных материй :-)
Всем добра, и не забывайте тестить бекенд :-)
Автор: kazeboba
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/447374
Ссылки в тексте:
[1] https://docs.mitmproxy.org/stable/overview/installation/: https://docs.mitmproxy.org/stable/overview/installation/
[2] https://docs.mitmproxy.org/stable/concepts/certificates/: https://docs.mitmproxy.org/stable/concepts/certificates/
[3] https://docs.mitmproxy.org/stable/overview/getting-started/: https://docs.mitmproxy.org/stable/overview/getting-started/
[4] https://github.com/kazeboba/automation-proxy.git: https://github.com/kazeboba/automation-proxy.git
[5] Источник: https://habr.com/ru/articles/1013218/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1013218
Нажмите здесь для печати.