Skip to main content
Back to course
Pytest: Профессиональные инструменты
6 / 875%

Создание pytest-плагинов

25 минут

Команде нужно автоматически пропускать тесты на 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.md

1. Код плагина

# 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():
    """Тестируем маркер плагина"""
    pass

4. Установка и использование

# Установка в 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=staging
def 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.

Создание pytest-плагинов — Pytest: Профессиональные инструменты — Potapov.me