|
Меня зовут Владислав Тимашенков, я занимаюсь автоматизацией тестирования в ГК InfoWatch. Мы разрабатываем DLP-систему для защиты контента и предотвращения утечек информации. Специфика продуктов InfoWatch состоит в анализе самых разнообразных данных. Поэтому практически каждый автотест требует подхода с широким набором параметров. Для нашей команды хорошим решением стало вынести генерацию параметров pytest.mark.parametrize в отдельный компонент, который собирает данные из файловой коллекции проекта. Существует множество способов параметризировать тестовые функции. Эта статья — пример изящной параметризации Pytest для сценариев, основанных на файловой коллекции проекта с большим количеством данных. Расскажем про наш опыт решения такой задачи. Параметризация Pytest позволяет сделать из теста каркас, шаблон, который принимает данные для конфигураций, для assert и т.д.
Пример из документации Pytest: @pytest.fixture def fixt(request): return request.param * 3
@pytest.mark.parametrize("fixt", ["a", "b"], indirect=True) def test_indirect(fixt): assert len(fixt) == 3
Для простых сценариев такая параметризация сработает, но что если нам требуется нечто большее, чем захардкоженные списки? Абстрагируемся от Pytest как от фреймворка и посмотрим на pytest.mark.parametrize как на объект-декоратор, принимающий набор аргументов. pytest.mark.parametrize( argnames, # str | Sequence[str] argvalues, # Iterable[Sequence[Any] | Any] indirect, # bool | Sequence[str] ids, # Iterable | Callable | None scope, # str | None )
Под капотом pytest.mark.parametrize — это объект с методом call, который принимает аргументы и возвращает декоратор тестовой функции. Его параметры можно передать через **kwargs, распаковав словарь формата {param_name: your_value, ...}. Вот тут и начинается самое интересное. Опишу пример на своей недавней задаче. Автотест проверяет срабатывание определенной настройки системы при перехвате разных событий (система умеет определять передачу данных, ввод на клавиатуре, копирование файлов, но я создаю эти события через внутренние инструменты тестирования). Требования к параметризации: Тест параметризуется данными из файловой коллекции парой: файл с событием — файл с ожидаемыми данными 1 параметр = 1 пара = 1 директория ID параметра = путь к директории (.../setting_name/positive/setting_action) Директории с параметрами должны иметь произвольную глубину вложенности Лаконичная и простая в поддержке реализация
Таким образом, у нас набирается список логик: Обойти файловую коллекцию Найти целевые директории Сформировать параметры: прочитать один файл, собрать путь к другому файлу Сформировать id параметров Собрать словарь в формате для Pytest.
При этом тест состоит всего из трёх шагов: отправить данные, запросить результаты анализа, увидеть в актуальном результате ожидаемый результат. Мы решили эту задачу следующим образом: import os
from pathlib import Path import pytest
def example_parametrizer(path: Path) -> dict: # Создаем пустые списки для хранения значений id и параметров тестов argvalues = [] ids = [] # Рекурсивный обход директории, начиная с указанного пути. # os.walk возвращает кортежи (путь_к_директории, список_поддиректорий, список_файлов) for dirpath, _, filenames in os.walk(path): dirpath = Path(dirpath) # Если файл expected.yaml в директории, то это нужная нам папка с данными if "expected.yaml" in filenames: # Создание пути к JSON-файлу для создания события dump_path = dirpath / "file_for_creating_event.json" # Чтение и парсинг YAML файла с ожидаемыми настройками expected_settings = some_yaml_reader(dirpath / "expected.yaml") # Добавление кортежа с отправляемыми и ожидаемыми данными в список значений argvalues.append((dump_path, expected_settings)) # Добавление относительного пути директории как идентификатора теста ids.append(os.path.relpath(dirpath, path)) # Возврат словаря с параметрами для декоратора pytest.mark.parametrize return { "argnames": ("dump_path", "expected_settings"), "argvalues": argvalues, "ids": ids, }
Структура файловой коллекции такова: automation_file_collection └── base_test_name ├── some_preconditions … └── test_cases ├── negative … └── positive └── param_for_setting1 ├── file_for_creating_event.json └── expected.yaml И используется example_parametrizer() следующим образом: # Путь к директории с тест-кейсами для положительных сценариев test_cases_path = automation_file_collection / "test_cases" / "positive" # Декоратор для параметризации теста создает тест-кейсы, **kwargs используется для распаковки словаря в именованные аргументы @pytest.mark.parametrize(**example_parametrizer(test_cases_path)) def test_detection_setting(dump_path, expected_settings): """ Описание теста ... """ # создаем событие event_data = creating_event_tool(dump_path) # получаем актуальные данные о работе целевой настройки actual_settings: set = set(event_data.model.settings) # сравниваем актуальные данные и ожидаемые из параметризации assert set(expected_settings).issubset( actual_settings ), f"Ожидаемые настройки {expected_settings} не найдены в событии {event_data}"
Пример вывода: test_detection_setting.py::test_detection_setting[param_for_setting1] PASSED test_detection_setting.py::test_detection_setting[param_for_setting2] PASSED test_detection_setting.py::test_detection_setting[param_for_setting3] PASSED
Что мы получили по итогу?Тест стал шаблоном, в его ответственность входит только выполнение действий, он не зависит от данных. Параметры набираются из любых папок в коллекции по указанному пути, а относительный путь — понятный идентификатор теста. Чтобы создать новый тест-кейс, нужно лишь добавить файлы в коллекцию. Реализация основана на базовых инструментах Python и не требует неочевидных и непопулярных методов Pytest.
Аналогичное решение применимо и к @pytest.fixture(). Под капотом мы увидим параметры scope, params (с поддержкой ids) и autouse. И можем написать такой же параметризатор, который вернет словарь с ключами для фикстуры: def fixture_parametrizer(): ids = [] params = [] # Ваша логика для генерации параметров params.append(...) ids.append(...) return {"params": params, "ids": ids}
Также существует альтернативный способ генерировать параметры через pytest_generate_tests. Но он имеет ряд недостатков: логика параметризации выносится из теста, усложняется инфраструктура тестов и снижается читаемость и простота поддержки.
Друзья, если вы ищете простой способ использовать объемную файловую коллекцию в качестве параметров тестов на Python+Pytest, то предлагаем вам свой опыт решения этой задачи.
Такой подход позволяет масштабировать тесты без усложнения самих тестовых функций. А добавление нового тест-кейса сводится к добавлению файлов в коллекцию, сама же логика теста и параметризации остаётся неизменной. Обсудить в форуме |