Перейти к содержимому
К программе курса
Pytest с нуля: тесты, которые реально работают
6 / 1155%

🏷️ Маркеры и структура: запускаем нужные тесты

28 минут

⚠️ Урок устарел. Свежая версия в курсе pytest-basics: Маркеры: запускаем нужные тесты.

Зачем это нужно

🐌 До: всегда гоняем всё — 10+ минут ожидания
После: запускаем ровно нужные тесты — 30 секунд

✅ Маркеры для категорий (slow/integration/api/bugfix)
pytest.ini для удобного запуска в CI и IDE
✅ Структура папок + conftest.py без хаоса

Маркеры: умный запуск тестов

import pytest
import sys
 
@pytest.mark.slow          # 🐌 Долгие (>1 с)
def test_performance():
    ...
 
@pytest.mark.integration   # 🏗️ БД/сервисы
def test_database_operations():
    ...
 
@pytest.mark.external_api  # 🌐 Внешний API
def test_payment_gateway():
    ...
 
@pytest.mark.skip(reason="Ждём фикс от API")  # ⏸️ Временно отключено
def test_broken_external_service():
    ...
 
@pytest.mark.skipif(sys.version_info < (3, 9), reason="Требует Python 3.9+")
def test_new_syntax():
    ...

pytest.ini:

[pytest]
markers =
    slow: медленные тесты (deselect with -m "not slow")
    integration: тесты с БД или сервисами
    external_api: вызовы внешних API
    bugfix: тесты для исправленных багов

Важно: без этих объявлений pytest будет предупреждать об неизвестных маркерах.

Умный запуск

pytest -m "not slow"      # Только быстрые
pytest -m integration     # Только интеграционные
pytest -m bugfix          # Багфиксы
pytest -rs                # Показывать пропущенные

Структура без хаоса

Простая структура

tests/
  test_todo.py
  test_user.py
  conftest.py

Расширенная структура и иерархия conftest

tests/
  unit/            # 🚀 Быстрые проверки (<100ms)
    test_todo.py
    test_user.py
    conftest.py    # 🔧 Фикстуры только для unit
  integration/     # 🏗️ Медленные/сеть/БД
    test_database.py
    test_api.py
    conftest.py    # 🔧 Фикстуры только для integration
  conftest.py      # 🔧 Общие фикстуры для всех тестов

Conftest.py: общие фикстуры и область видимости

import pytest
from src.task_manager import TaskManager
 
@pytest.fixture
def sample_tasks():
    return ["task1", "task2"]
 
@pytest.fixture
def empty_task_manager():
    return TaskManager()

tests/unit/conftest.py (виден только для unit-тестов в этой папке):

import pytest
 
@pytest.fixture
def mock_database():
    return MockDatabase()  # моки только для unit-тестов

tests/integration/conftest.py (виден только для integration):

import pytest
from myapp.db import create_connection
 
@pytest.fixture
def db_connection():
    conn = create_connection(testing=True)
    yield conn
    conn.close()

Pytest сам подхватывает conftest по иерархии: общий tests/conftest.py доступен всем, вложенные — только своим подпапкам. Это убирает дублирование и исключает ручные импорты фикстур.

Практика: размечаем и структурируем

import pytest, sys
 
@pytest.mark.unit  # 🚀 Быстрый unit-тест
def test_fast_unit():
    pass
 
@pytest.mark.integration  # 🏗️ Интеграция с БД
def test_database_query():
    pass
 
@pytest.mark.external_api  # 🌐 Внешний API
@pytest.mark.slow          # 🐌 Долгий
def test_external_payment_api():
    pass
 
@pytest.mark.skip(reason="База в maintenance")
def test_database_maintenance():
    pass

Команды для повседневного запуска

pytest -m "not slow and not integration" --tb=short   # Разработка
pytest -m "not external_api" --junitxml=report.xml    # CI без внешних API
pytest --durations=10 -m "slow"                       # Поиск медленных
pytest tests/unit/ -v                                 # Только unit

Реальные CI/CD сценарии

Пример GitHub Actions с раздельными джобами:

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - run: pytest -m "not slow and not integration" --junitxml=unit-report.xml
 
  integration-tests:
    runs-on: ubuntu-latest
    needs: unit-tests
    steps:
      - run: pytest -m integration --junitxml=integration-report.xml

❌ Анти-паттерн: хаос в тестах

tests/
  test_everything.py      # 1000 строк вперемешку
  test_utils.py           # Непонятно, что проверяется
  helpers.py              # Фикстуры разбросаны

Результат: сложно найти нужный тест, дубли фикстур, на CI гоняются все тесты всегда.

Конфигурация для команды: pytest.ini vs pyproject.toml

Проблема: Каждый член команды запускает pytest со своими флагами. Результаты отличаются локально и в CI.

Решение: Единая конфигурация в pytest.ini или pyproject.toml.

Вариант 1: pytest.ini (классика)

# pytest.ini в корне проекта
[pytest]
# Где искать тесты
testpaths = tests
 
# Какие файлы считать тестами
python_files = test_*.py *_test.py
 
# Какие классы считать тестовыми
python_classes = Test*
 
# Какие функции считать тестами
python_functions = test_*
 
# Дефолтные флаги для всех запусков
addopts =
    -v
    --strict-markers
    --tb=short
    --cov=src
    --cov-report=term-missing
    --cov-fail-under=70
 
# Регистрация маркеров (обязательно с --strict-markers)
markers =
    slow: медленные тесты (deselect with '-m "not slow"')
    integration: тесты с БД или сервисами
    external_api: вызовы внешних API
    unit: быстрые unit-тесты
    bugfix: тесты для исправленных багов

Вариант 2: pyproject.toml (современный)

# pyproject.toml в корне проекта
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
 
addopts = [
    "-v",
    "--strict-markers",
    "--tb=short",
    "--cov=src",
    "--cov-report=term-missing",
    "--cov-fail-under=70",
]
 
markers = [
    "slow: медленные тесты (deselect with '-m \"not slow\"')",
    "integration: тесты с БД или сервисами",
    "external_api: вызовы внешних API",
    "unit: быстрые unit-тесты",
    "bugfix: тесты для исправленных багов",
]

Что даёт конфигурация?

До конфигурации:

# Каждый раз вводим вручную
pytest -v --strict-markers --cov=src --cov-report=term-missing tests/

После конфигурации:

# Просто pytest — все флаги подхватываются из конфига
pytest
 
# Переопределяем при необходимости
pytest -q  # тихий режим вместо -v
pytest --no-cov  # без coverage

Практика: создайте конфигурацию

Задача: Настройте pyproject.toml для своего проекта.

# Если у вас уже есть pyproject.toml — добавьте секцию
# Если нет — создайте файл
 
cat >> pyproject.toml << 'EOF'
 
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = [
    "-v",
    "--strict-markers",
    "--tb=short",
]
markers = [
    "slow: медленные тесты",
    "integration: интеграционные тесты",
    "unit: unit-тесты",
]
EOF
 
# Проверьте
pytest --markers  # должны увидеть ваши маркеры
pytest  # запуск с дефолтными флагами

⚡ Параллельный запуск с pytest-xdist

Проблема: 1000 тестов по 0.1 секунды = 100 секунд ожидания. Ваш компьютер простаивает на 75%.

Решение: pytest-xdist запускает тесты параллельно на всех ядрах процессора.

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

# Установка
pip install pytest-xdist
 
# Автоматически использует все ядра
pytest -n auto
 
# Или явно укажите количество процессов
pytest -n 4
 
# С coverage (работает корректно)
pytest -n auto --cov=src
 
# С маркерами
pytest -n auto -m "not slow"

Экономия времени (реальные цифры)

# Последовательный запуск (1 ядро)
pytest tests/
# 1000 тестов × 0.1 сек = 100 секунд
 
# Параллельный запуск (4 ядра)
pytest -n 4 tests/
# 1000 тестов ÷ 4 ядра × 0.1 сек = 25 секунд
 
# Автоматически (8 ядер на M1/M2 Mac)
pytest -n auto tests/
# 1000 тестов ÷ 8 ядер × 0.1 сек = 12.5 секунд
 
# ⚡ 8x ускорение!

Когда использовать xdist

Используйте:

  • Unit-тесты (независимые, быстрые)
  • Большое количество тестов (100+)
  • Разработка (локальный запуск)
  • CI/CD (ускорение пайплайна)

Не используйте:

  • Тесты с общим состоянием (БД без изоляции)
  • Тесты с race conditions
  • Один тест занимает 90% времени (бутылочное горлышко)

Распределение тестов (loadscope vs loadfile)

# По умолчанию: loadfile (файлы распределяются по процессам)
pytest -n 4
 
# loadscope: тесты одного класса идут на один процесс
pytest -n 4 --dist loadscope
 
# loadgroup: группировка по маркерам
pytest -n 4 --dist loadgroup

Добавьте в конфигурацию

# pyproject.toml
[tool.pytest.ini_options]
addopts = [
    "-n auto",  # Всегда параллельно
    "-v",
    "--strict-markers",
]
 
# Или для CI:
# addopts = ["-n 4"]  # Фиксированное количество

Частые вопросы

Вопрос 1: Почему тесты падают при -n auto? Ответ: Проверьте изоляцию:

  • Тесты используют общую БД без транзакций?
  • Модифицируют глобальные переменные?
  • Пишут в одни файлы без уникальных имён?

Вопрос 2: Тесты стали медленнее с -n auto. Почему? Ответ: Overhead при запуске процессов. Если тестов < 10 и они быстрые (< 0.01s), параллелизм не даст выигрыша.

Вопрос 3: Работает ли coverage с xdist? Ответ: Да! pytest -n auto --cov=src работает корректно. Coverage собирается из всех процессов.

Чеклист организации

  • Маркеры расставлены: slow/integration/api/bugfix
  • Логическая структура: unit/integration + общий conftest.py
  • Создана конфигурация (pytest.ini или pyproject.toml)
  • Маркеры зарегистрированы с --strict-markers
  • Установлен pytest-xdist для параллельного запуска
  • Тесты изолированы (проходят с -n auto)
  • Понимаете, какие тесты гонять локально, какие — в пайплайне

Запускайте только то, что нужно, параллельно и экономьте время команды. 🚀

🏷️ Маркеры и структура: запускаем нужные тесты — Pytest с нуля: тесты, которые реально работают — Potapov.me