Hooks, conftest как плагин, pytest_configure, pytest_collection_modifyitems. Автоматизация для команды
Команде нужно автоматически пропускать тесты на Windows, логировать длительные тесты, сортировать по приоритету. Писать это в каждом conftest.py? Есть способ лучше!
Цель: Научиться создавать pytest плагины для автоматизации команды.
Убедитесь, что умеете:
# conftest.py и фикстуры
@pytest.fixture
def resource():
return Resource()
# Маркеры
@pytest.mark.slow
def test_something():
passЕсли conftest.py или маркеры непонятны — вернитесь к предыдущим урокам.
Плагин — это код который расширяет 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 — это плагин! Можно определять hooks прямо в нём.
# 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 ==========================
# 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")# 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# 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")# 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# 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)# 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.md
# 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 *# 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] регистрирует плагин!
# 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():
"""Тестируем маркер плагина"""
pass# Установка в editable режиме
pip install -e .
# Проверка что плагин установлен
pytest --version
# pytest 7.x.x
# plugins: my-plugin-0.1.0
# Использование
pytest tests/✅ Плагин работает автоматически!
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"def pytest_generate_tests(metafunc):
"""Динамически параметризуем тесты"""
if "browser" in metafunc.fixturenames:
browsers = metafunc.config.getoption("--browsers", "chrome").split(",")
metafunc.parametrize("browser", browsers)def pytest_sessionfinish(session, exitstatus):
"""Вызывается в конце сессии"""
print(f"\n✅ Tests finished with status: {exitstatus}")
print(f"Total tests: {len(session.items)}")Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: Coverage thresholds и enforcement