Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → pytest: фикстуры, параметризация и плагины

pytest на практике: фикстуры, параметризация и плагины без магии

Как писать читаемые pytest-тесты: фикстуры и их области видимости, параметризация вместо дублирования, полезные плагины (cov, mock, testcontainers), структура тестов и типовые ошибки.

Прямой ответ

Три механизма покрывают большинство потребностей pytest. Фикстуры — подготовка и разбор контекста теста (объекты, БД, моки) с инъекцией по имени аргумента и областями видимости (function/class/module/session). Параметризация — одна тест-функция, много наборов вход/ожидание: @pytest.mark.parametrize убирает копипасту и даёт честный отчёт «какой именно случай упал». Плагины — готовые расширения под задачу: cov для покрытия, mock для изоляции, testcontainers для реальных сервисов. Принцип, который отличает хорошие тесты: фикстура делает один контекст, тест проверяет одно поведение, а связь между ними читается из имён.

Фикстуры: контекст вместо сетапа в каждом тесте

Фикстура — функция с @pytest.fixture, значение которой приходит в тест по имени параметра. Преимущества перед сетапом в теле теста: переиспользование, ленивость (не создаётся, пока не попросят), и главное — finalization через yield: код после yield выполнится как разбор, даже если тест упал (аналог teardown, но рядом с сетапом). Области видимости — главный рычаг скорости: scope="function" (дефолт, свежая на каждый тест) — безопасно; module/session — для дорогих неизменяемых ресурсов (подключение к тестовой БД, загруженная модель), но требуют дисциплины: общая изменяемая фикстура — источник «плавающих» тестов, где порядок запуска влияет на результат. Правило: изменяемое — function-scope, дорогое и неизменяемое — выше, а «грязное общее» не делать общим.

Параметризация: таблица случаев вместо копипасты

@pytest.mark.parametrize("вход, ожидание", [...]) превращает тест в таблицу: каждый набор — отдельный кейс в отчёте с понятным id. Приёмы, которые отличают зрелое использование: осмысленные ids (ids=["пустой список", "дубликаты"] — падение читается без раскрытия кортежа); непрямая параметризация фикстур (@pytest.mark.parametrize("db", [...], indirect=True) — параметры попадают в фикстуру, полезно для конфигураций окружения); стэкинг нескольких parametrize (декартово произведение — осторожно с взрывом числа кейсов); pytest.param(..., marks=pytest.mark.xfail/xfail/skipif) для кейсов-исключений прямо в таблице. Антипаттерн — параметризация «всего», когда разные наборы проверяют разное поведение: такой тест после падения непонятен, лучше разбить на два теста с собственными таблицами.

Структура и именование: тест как документация

Рабочие конвенции: файл test_<модуль>.py рядом по структуре (или tests/ зеркалом пакета), классы Test<Фича> для группировки общих фикстур, имена тестов test_<поведение>_<условие> (test_login_wrong_password_returns_401). Тело — паттерн AAA (Arrange–Act–Assert) с пустыми строками между фазами: тест читается как текст. Assert-сообщения pytest показывает с полным диффом значений — «assert 5 == 4» с контекстом переменных, отдельный assertEqual-стиль не нужен. conftest.py — место общих фикстур (видимость — по директории), но разросшийся conftest на 500 строк с фикстурами «на все случаи» — антипаттерн: фикстуры живут ближе к использованию.

Плагины, которые реально нужны

Минимальный полезный набор: pytest-cov (покрытие --cov=pkg --cov-report=term-missing — цифра сама по себе не цель, но пропущенные ветки видны сразу); pytest-mock (обёртка над unittest.mock — удобные моки через фикстуру mocker, приёмы мокирования — в статье про мок и патч); pytest-xdist (-n auto — параллельный запуск, заметно на больших сюитах); pytest-testmon/--lf (запуск только затронутых/упавших — быстрая локальная петля); pytest-asyncio (async-тесты — см. статью про asyncio-практику). Для интеграционных тестов с реальными сервисами — testcontainers (поднятие БД/редиса в docker на время прогона — разбор в статье про Testcontainers); в CI матрица версий Python настраивается отдельно — гайд про GitHub Actions.

Типовые ошибки

Первая — тесты, зависящие от порядка: общие session-фикстуры с состоянием, «счастливая» последовательность; лечится изоляцией и случайным порядком (плагин random-order как детектор). Вторая — мокирование собственного кода вместо внешних границ: тесты проходят, интеграция ломается; мокать — сети/БД/время, не свои функции. Третья — фикстура на 80 строк, создающая «весь мир»: тест не падает на её ошибке, но и не объясняет, что нужно тесту; дробите на композицию мелких фикстур (фикстура может запрашивать другие фикстуры). Четвёртая — assert без сообщения о намерении: assert result.status == "failed" информативнее assert result, а для сложных объектов — сравнение конкретных полей, а не всего объекта, если суть в полях.

Практический сценарий: тесты функции расчёта скидки

Покажем связку фикстур и параметризации на компактном примере. Функция calc_discount(cart, promo) принимает корзину (dataclass со списком позиций) и промокод, возвращает сумму скидки; внутри — обращения к каталогу и внешнему сервису промокодов. Фикстура-фабрика make_cart возвращает функцию сборки корзины с параметрами (позиции, количество) — тесты читаются как спецификация: cart = make_cart(items=[("A", 2), ("B", 1)]). Каталог — фикстура session-scope (неизменяемый словарь загружается один раз на прогон), сервис промокодов — mocker.patch на границе модуля с подготовленными ответами. Параметризация покрывает таблицу случаев: обычный промокод, истёкший, неприменимый к корзине, пустая корзина, сумма ниже порога — @pytest.mark.parametrize("promo, expected", [...], ids=["валидный", "истёкший", "чужая категория", "пустая корзина", "ниже порога"]) — при падении в отчёте видно «истёкший», а не «case3». Отдельный тест проверяет взаимодействие: для валидного промокода сервис вызван ровно один раз с нужным кодом (mocker assert) — это тот случай, когда assert_called уместен, потому что взаимодействие и есть контракт. Весь набор — пятнадцать кейсов, исполняется за миллисекунды, и каждый падает с понятным именем.

FAQ

Когда фикстура, а когда просто функция-хелпер? Если нужна подготовка без разбора и без инъекции — достаточно функции, вызываемой в тесте (явнее и проще для чтения). Фикстура оправдана для finalization (yield), ленивости, области видимости и композиции.

Чем parametrize отличается от цикла assert внутри теста? Параметризация даёт отдельные кейсы в отчёте: видно, какой именно набор упал, и остальные продолжают выполняться. Цикл падает на первом же случае и скрывает картину.

Стоит ли гнаться за 100% покрытием? Нет: покрытие — индикатор непроверенных мест, а не цель. 100% достижимо тестами без единого осмысленного assert; ориентир — покрытие критичных веток и логики, а не цифра.

Нужен ли unittest, если есть pytest? Для нового кода — нет: pytest совместим со старыми unittest-классами, но его фикстуры/параметризация/отчёты объективно удобнее. unittest остаётся в легаси и там, где нельзя ставить зависимости.

Источники: документация pytest (fixtures, parametrize, plugin list) — ссылки в шапке.