Создание pytest-плагинов
Команде нужно автоматически пропускать тесты на Windows, логировать длительные тесты, сортировать по приоритету. Писать это в каждом conftest.py? Есть способ лучше!
Цель: Научиться создавать pytest плагины для автоматизации команды.
Вы точно готовы?
Убедитесь, что умеете:
# conftest.py и фикстуры
@pytest.fixture
def resource():
return Resource()
# Маркеры
@pytest.mark.slow
def test_something():
passЕсли conftest.py или маркеры непонятны — вернитесь к предыдущим урокам.
Что такое pytest плагины
Плагины = hooks + фикстуры
Плагин — это код который расширяет pytest через hooks.
Hook — функция которая вызывается pytest в определённые моменты:
pytest start
↓
pytest_configure() # Конфигурация
↓
pytest_collection() # Сбор тестов
↓
pytest_collection_modifyitems() # Модификация списка тестов
↓
pytest_runtest_setup() # Setup перед тестом
↓
test execution # Тест
↓
pytest_runtest_teardown() # Teardown после теста
↓
pytest_sessionfinish() # Завершение сессииconftest.py как плагин
conftest.py = локальный плагин
Любой conftest.py — это плагин! Можно определять hooks прямо в нём.
Hook: pytest_configure
# conftest.py
def pytest_configure(config):
"""Вызывается при старте pytest"""
print("\n🚀 Pytest starting...")
# Добавляем кастомный атрибут
config.custom_value = "Hello from plugin"
# Регистрируем маркеры программно
config.addinivalue_line(
"markers",
"custom: Custom marker added by plugin"
)Запуск:
pytestРезультат:
🚀 Pytest starting...
========================= test session starts ==========================Hook: pytest_collection_modifyitems
# conftest.py
def pytest_collection_modifyitems(config, items):
"""Модифицирует список собранных тестов"""
print(f"\n📋 Collected {len(items)} tests")
# Сортируем тесты по имени
items.sort(key=lambda item: item.name)
# Добавляем маркер ко всем тестам
for item in items:
item.add_marker("analyzed")Практические примеры
Пример 1: Автоматический skip на Windows
# conftest.py
import sys
import pytest
def pytest_collection_modifyitems(items):
"""Пропускаем unix_only тесты на Windows"""
if sys.platform == "win32":
skip_windows = pytest.mark.skip(reason="Unix only")
for item in items:
if "unix_only" in item.keywords:
item.add_marker(skip_windows)Использование:
# tests/test_unix.py
@pytest.mark.unix_only
def test_unix_feature():
"""Автоматически пропустится на Windows"""
passПример 2: Логирование медленных тестов
# conftest.py
import pytest
import time
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item):
"""Замеряем время выполнения теста"""
start = time.time()
yield
duration = time.time() - start
if duration > 1.0:
print(f"\n⚠️ Slow test: {item.name} took {duration:.2f}s")Пример 3: Сортировка по приоритету
# conftest.py
def pytest_collection_modifyitems(items):
"""Сортируем тесты по приоритету"""
priorities = {"critical": 0, "high": 1, "medium": 2, "low": 3}
def priority_key(item):
marker = item.get_closest_marker("priority")
if marker and marker.args:
return priorities.get(marker.args[0], 99)
return 99
items.sort(key=priority_key)Использование:
@pytest.mark.priority("critical")
def test_critical_feature():
pass
@pytest.mark.priority("low")
def test_nice_to_have():
passПример 4: Автоматические маркеры по пути
# conftest.py
def pytest_collection_modifyitems(items):
"""Добавляем маркеры по пути к файлу"""
for item in items:
# Если тест в tests/integration/
if "integration" in str(item.fspath):
item.add_marker(pytest.mark.integration)
# Если тест в tests/unit/
if "unit" in str(item.fspath):
item.add_marker(pytest.mark.unit)Пример 5: Кастомный reporter
# conftest.py
def pytest_runtest_logreport(report):
"""Вызывается после каждого теста"""
if report.when == "call":
if report.passed:
print(f"✅ {report.nodeid}")
elif report.failed:
print(f"❌ {report.nodeid}")Создание пакета плагина
Структура плагина
my-pytest-plugin/
├── pytest_my_plugin/
│ ├── __init__.py
│ └── plugin.py
├── tests/
│ └── test_plugin.py
├── pyproject.toml
└── README.md1. Код плагина
# pytest_my_plugin/plugin.py
import pytest
def pytest_configure(config):
"""Регистрируем кастомные маркеры"""
config.addinivalue_line(
"markers",
"team: Tests for team feature"
)
def pytest_collection_modifyitems(items):
"""Сортируем тесты по имени"""
items.sort(key=lambda x: x.name)
@pytest.fixture
def team_data():
"""Фикстура от плагина"""
return {"name": "TeamA", "members": 5}# pytest_my_plugin/__init__.py
from .plugin import *2. pyproject.toml
# pyproject.toml
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "pytest-my-plugin"
version = "0.1.0"
description = "My awesome pytest plugin"
authors = [{name = "Your Name", email = "you@example.com"}]
requires-python = ">=3.8"
dependencies = [
"pytest>=7.0.0",
]
# Регистрируем плагин в pytest
[project.entry-points.pytest11]
my_plugin = "pytest_my_plugin.plugin"Ключевая часть: [project.entry-points.pytest11] регистрирует плагин!
3. Тесты для плагина
# tests/test_plugin.py
def test_team_fixture(team_data):
"""Тестируем фикстуру плагина"""
assert team_data["name"] == "TeamA"
assert team_data["members"] == 5
@pytest.mark.team
def test_team_marker():
"""Тестируем маркер плагина"""
pass4. Установка и использование
# Установка в editable режиме
pip install -e .
# Проверка что плагин установлен
pytest --version
# pytest 7.x.x
# plugins: my-plugin-0.1.0
# Использование
pytest tests/✅ Плагин работает автоматически!
Популярные hooks (bonus)
pytest_addoption — CLI опции
def pytest_addoption(parser):
"""Добавляем кастомную CLI опцию"""
parser.addoption(
"--env",
action="store",
default="test",
help="Environment: test, staging, prod"
)
@pytest.fixture
def env(request):
"""Фикстура для доступа к --env"""
return request.config.getoption("--env")Использование:
pytest --env=stagingdef test_api(env):
assert env == "staging"pytest_generate_tests — динамическая параметризация
def pytest_generate_tests(metafunc):
"""Динамически параметризуем тесты"""
if "browser" in metafunc.fixturenames:
browsers = metafunc.config.getoption("--browsers", "chrome").split(",")
metafunc.parametrize("browser", browsers)pytest_sessionfinish — финальные действия
def pytest_sessionfinish(session, exitstatus):
"""Вызывается в конце сессии"""
print(f"\n✅ Tests finished with status: {exitstatus}")
print(f"Total tests: {len(session.items)}")Что вы изучили
- Hooks system — расширение pytest через hooks
- conftest.py — локальный плагин для проекта
- pytest_configure — настройка при старте
- pytest_collection_modifyitems — модификация тестов
- Создание пакета — плагин как Python package
- entry_points — регистрация плагина в pytest
Следующий урок
Отлично! Теперь вы умеете создавать плагины. Но как установить минимальное покрытие и заблокировать CI если тесты плохие?
Переходите к уроку 6: Coverage thresholds и enforcement
В следующем уроке вы узнаете:
--cov-fail-underдля минимального порога- Интеграция coverage в CI
- Quality gates
- Per-module coverage
Устранение неисправностей
Pytest не находит или не видит ваш hook.
Проверьте:
- Название hook точное:
pytest_configure, а неpytest_config - Hook лежит в
conftest.pyили зарегистрированном плагине - Нет опечаток в имени функции
Проверьте регистрацию в pyproject.toml:
[project.entry-points.pytest11]
my_plugin = "pytest_my_plugin.plugin" # ✅ Правильно
Если правили код — переустановите:
pip install -e . --force-reinstallЗапустите pytest без него:
pytest -p no:my_pluginЛогируйте то, что происходит внутри hook.
def pytest_collection_modifyitems(items):
print(f"Items: {[item.name for item in items]}")
Для продакшена лучше использовать logging, а не print.