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% строк не гарантируют проверку важных случаев.