pytest

Материал из llmdocs — справочник разработчика
Портал: Python
pytest
Типфреймворк тестирования
АвторХольгер Крекель
Появился2004 (как py.test)
ЛицензияMIT
Ключевые возможностиassert с подробными отчётами, фикстуры, параметризация, маркеры
Плагиныpytest-cov, pytest-xdist, pytest-asyncio, pytest-mock, hypothesis
Альтернативаunittest (стандартная библиотека)

pytest — фреймворк для написания и запуска тестов на Python, самый популярный в экосистеме. Тест в pytest — обычная функция, имя которой начинается с test_, а проверки — обычный оператор assert; при падении pytest показывает подробности: какие значения сравнивались и чем они отличаются. Мощь фреймворка — в фикстурах для подготовки окружения, параметризации для прогона одного теста на многих наборах данных и сотнях плагинов.

pytest используют и для модульных тестов, и для интеграционных, и для проверки API, баз данных, веб-интерфейсов; он запускает и тесты, написанные на стандартном unittest.

История

Хольгер Крекель начал разработку в 2004 году в рамках проекта PyPy под названием py.test — частью библиотеки py. Позже pytest выделился в самостоятельный проект, команда запуска стала называться pytest, а сам фреймворк вытеснил unittest в большинстве новых проектов. Сейчас pytest развивает сообщество под организацией pytest-dev, распространяется под лицензией MIT.[1]

Первые тесты

# test_pricing.py
import pytest
from shop.pricing import apply_discount

def test_discount_reduces_price():
    assert apply_discount(1000, percent=10) == 900

def test_negative_percent_rejected():
    with pytest.raises(ValueError, match="percent"):
        apply_discount(1000, percent=-5)
uv add --dev pytest
uv run pytest                 # найдёт файлы test_*.py и функции test_*
uv run pytest -x -q           # остановиться на первой ошибке, краткий вывод
uv run pytest -k discount     # только тесты, в имени которых есть «discount»
uv run pytest --lf            # перезапустить упавшие в прошлый раз

pytest переписывает assert при импорте тестов, поэтому сообщение об ошибке показывает обе стороны сравнения, а для списков, словарей и строк — построчную разницу. Не нужно запоминать десятки методов вроде assertEqual, assertIn, assertDictEqual, как в unittest.[4][5]

Фикстуры

Фикстура — функция, которая готовит ресурс для теста и при необходимости убирает его после. Тест получает фикстуру, просто указав её имя в параметрах:[2]

import pytest

@pytest.fixture
def db(tmp_path):
    conn = connect(tmp_path / "test.sqlite")   # tmp_path — встроенная фикстура: временный каталог
    conn.migrate()
    yield conn                                 # всё до yield — подготовка, после — очистка
    conn.close()

@pytest.fixture
def user(db):                                  # фикстуры могут зависеть от других фикстур
    return db.create_user(email="alice@example.com")

def test_user_can_place_order(db, user):
    order = db.create_order(user_id=user.id, total=500)
    assert order.status == "new"
  • Область действия (scope="function" | "module" | "session") задаёт, как часто создаётся фикстура: например, одна база данных на весь прогон и транзакция с откатом на каждый тест.
  • conftest.py — файл с общими фикстурами для каталога и вложенных каталогов, без импортов в тестах.
  • Встроенные фикстуры: tmp_path (временный каталог), monkeypatch (подмена атрибутов и переменных окружения), capsys (перехват вывода), caplog (логи).

Параметризация

@pytest.mark.parametrize(
    ("price", "percent", "expected"),
    [
        (1000, 0, 1000),
        (1000, 10, 900),
        (999, 33, 669),
        pytest.param(1000, 100, 0, id="full-discount"),
    ],
)
def test_apply_discount(price, percent, expected):
    assert apply_discount(price, percent=percent) == expected

Каждый набор становится отдельным тестом с собственным именем в отчёте, поэтому видно, какие именно случаи упали.[3]

Маркеры и конфигурация

Маркеры помечают тесты: @pytest.mark.skip, @pytest.mark.skipif(sys.platform == "win32", reason=...), @pytest.mark.xfail для известных ошибок и пользовательские метки вроде slow или integration, чтобы запускать группы тестов (pytest -m "not slow"). Настройки хранятся в pyproject.toml:

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra --strict-markers"
markers = ["slow: долгие тесты", "integration: требуют внешних сервисов"]

Плагины

Плагин Назначение
pytest-cov покрытие кода тестами
pytest-xdist параллельный запуск на нескольких ядрах (-n auto)[6]
pytest-asyncio, anyio тесты асинхронного кода (asyncio)
pytest-mock удобная обёртка над unittest.mock
hypothesis тестирование на основе свойств: генерирует сотни случайных входов и находит граничные случаи
pytest-django, pytest-httpx интеграция с фреймворками и HTTP-клиентами

Практика

  • Тесты запускаются в CI/CD при каждом pull request; быстрые модульные тесты — отдельно от медленных интеграционных.
  • Один тест — одно поведение; имя теста описывает ожидаемый результат.
  • Внешние сервисы в модульных тестах подменяют, а интеграционные тесты гоняют против настоящих зависимостей, например в контейнерах Docker.
  • Покрытие — ориентир, а не цель: 100% строк не гарантируют проверку важных случаев.

См. также

Источники

  1. pytest — документация
  2. pytest — How to use fixtures
  3. pytest — How to parametrize fixtures and test functions
  4. pytest — How to write and report assertions in tests
  5. Python — unittest
  6. pytest-xdist — документация