Специфика продуктов состоит в анализе самых разнообразных данных. Поэтому практически каждый автотест требует подхода с широким набором параметров. Для нашей команды хорошим решением стало вынести генерацию параметров pytest.mark.parametrize в отдельный компонент, который собирает данные из файловой коллекции проекта.
Привет, Хабр!
Меня зовут Владислав Тимашенков, я занимаюсь автоматизацией тестирования в ГК 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, то предлагаем вам свой опыт решения этой задачи.
Такой подход позволяет масштабировать тесты без усложнения самих тестовых функций. А добавление нового тест-кейса сводится к добавлению файлов в коллекцию, сама же логика теста и параметризации остаётся неизменной.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | postman2pytest: как превратить Postman-коллекцию в pytest-набор за одну команду | 0 | 12.88 | 14-05-2026 |
| 2 | Почему автотесты пропускают изменения в API и как это исправить с Pydantic | 0 | 5.93 | 26-04-2026 |
| 3 | Параллелизация pytest: от xdist до Kubernetes | 0 | 8.8 | 28-01-2026 |
| 4 | Как писать юнит-тесты, которые не ломаются | 0 | 9.59 | 21-02-2026 |
| 5 | Архитектура тестового фреймворка | 0 | 9.5 | 27-04-2026 |
| 6 | Как мы создали новый тестовый фреймворк, адаптируемый к росту проектов | 0 | 6.14 | 11-04-2026 |
| 7 | Фабрики в тестировании (Python, Django, pytest, factory_boy) | 0 | 5.34 | 03-04-2026 |
| 8 | Как тестировать внешние API в Python: от DI-мока до настоящего HTTP-сервера | 0 | 9.72 | 24-05-2026 |
| 9 | Как организовать тестовую среду, сохраняя покой владельца данных | 0 | 7.16 | 25-02-2026 |
| 10 | Playwright не спасает от флапающих тестов: разбираемся, как он ждёт на самом деле | 0 | 8.49 | 28-07-2026 |