pytest на практике: фикстуры, параметризация и моки
Три инструмента pytest, из-за которых тесты либо читаются за минуту, либо превращаются в копипасту: разбираем, где каждый уместен и чем вы платите за удобство.

Тесты пишут все, кто хочет спать спокойно после деплоя. Но между «тесты есть» и «тестам можно верить» лежит пропасть: фикстура, которая делится состоянием между тестами и ломает их порядок; параметризация, которая раздувает один прогон до пятисот случаев без единой осмысленной проверки; мок, который зафиксировал реализацию, а не поведение, и падает при любом рефакторинге. Разберём три главных инструмента pytest на конкретных случаях: когда они помогают, а когда добавляют боли. Всё написано для pytest 8.x и Python 3.11+; для моков используется pytest-mock поверх стандартного unittest.mock.
Фикстуры: подготовка данных без copy-paste в каждом тесте
Фикстура в pytest — функция с декоратором @pytest.fixture, результат которой тест получает через аргумент с тем же именем. Очевидное решение — готовить объект прямо в тесте. Оно работает ровно до третьего теста, где та же подготовка повторяется, и до момента, когда объект нужно потом закрыть.
import pytest
@pytest.fixture
def temp_db(tmp_path):
# tmp_path — встроенная фикстура: путь к временной директории теста
db_file = tmp_path / "test.sqlite"
connection = create_connection(db_file)
connection.execute("CREATE TABLE users (id INTEGER, name TEXT)")
yield connection # тест выполняется здесь
connection.close() # код после yield — это teardown
def test_insert_user(temp_db):
temp_db.execute("INSERT INTO users VALUES (1, 'Anna')")
row = temp_db.execute("SELECT name FROM users WHERE id = 1").fetchone()
assert row[0] == "Anna"
Всё до yield — подготовка, всё после — уборка. Уборка выполняется, даже если тест упал: pytest гарантирует это через контекст, а не через try/finally в теле теста.
Область видимости решает, сколько раз всё создаётся заново
Параметр scope определяет, как часто pytest пересоздаёт фикстуру. По умолчанию function — новый экземпляр на каждый тест. Это безопасно, но медленно, если внутри поднимается что-то тяжёлое.
| scope | Создаётся заново | Когда брать |
|---|---|---|
function | Перед каждым тестом | По умолчанию; данные, которые тест меняет |
class | Один раз на класс тестов | Общий контекст для группы методов |
module | Один раз на файл | Соединение, которое тесты только читают |
session | Один раз на весь прогон | Дорогой ресурс: контейнер, HTTP-сервер |
Широкий scope ускоряет прогон, но роняет изоляцию. Если один тест меняет объект из session-фикстуры, а следующий на это состояние опирается — вы получите тест, который проходит в одиночку и падает в общем прогоне. Такой баг ищут часами.
Правило простое: если тест хоть что-то пишет в объект фикстуры, держите function. Широкий scope — только для того, что тесты исключительно читают.
conftest.py — общие фикстуры без импортов
Фикстуру, нужную нескольким файлам, кладут в conftest.py рядом с тестами. pytest подхватывает её автоматически, импортировать не нужно. Файл действует на свою директорию и вложенные — так корневой conftest.py держит фикстуры для всего проекта, а вложенный переопределяет их для подпапки.
Параметризация: один тест вместо десяти копий
Когда логика одна, а входные данные разные, копировать тело теста — прямой путь к тому, что при изменении проверки вы поправите три копии из пяти. Декоратор @pytest.mark.parametrize прогоняет один тест на наборе входов, и каждый набор в отчёте виден как отдельный случай.
import pytest
@pytest.mark.parametrize(
"amount, rate, expected",
[
(1000, 0.2, 200), # обычный случай
(0, 0.2, 0), # нулевая сумма
(1000, 0, 0), # нулевая ставка
(999, 0.2, 199.8), # дробный результат
],
)
def test_calc_tax(amount, rate, expected):
assert calc_tax(amount, rate) == pytest.approx(expected)
pytest.approx здесь не украшение: сравнивать float через == нельзя, 0.1 + 0.2 не равно 0.3. При падении pytest покажет, какой именно набор упал — например test_calc_tax[999-0.2-199.8], а не абстрактное «тест не прошёл».
Где параметризация выходит боком
Соблазн — засунуть в список все мыслимые комбинации. Два параметра по десять значений через вложенные parametrize дают сто прогонов, из которых осмысленны пять: граничные и один типичный. Остальные девяносто пять тратят время и маскируют настоящую проверку за шумом. Параметризуйте случаи, которые действительно ведут себя по-разному, а не декартово произведение ради галочки.
Моки: изоляция от того, что вы не тестируете
Тест не должен ходить в реальный платёжный шлюз или ждать ответа стороннего API. Мок подменяет такой вызов заглушкой с заданным ответом. Через плагин pytest-mock это делается фикстурой mocker — она сама откатывает подмену после теста, в отличие от ручного patch, где легко забыть про уборку.
def test_notify_sends_email(mocker):
# подменяем функцию отправки в том модуле, где она ВЫЗЫВАЕТСЯ
mock_send = mocker.patch("myapp.notifications.send_email")
notify_user(user_id=42, text="Заказ готов")
mock_send.assert_called_once_with(
to="anna@example.com",
subject="Заказ готов",
)
Ключевая ошибка новичков — патчить не там, где объект определён, а не там, где он используется. Патчить нужно путь, по которому имя ищется в момент вызова. Если notifications.py делает from myapp.mail import send_email, то мокать надо myapp.notifications.send_email, а не myapp.mail.send_email — иначе подмена не сработает, а тест позеленеет, ничего на самом деле не проверив.
Возвращаемое значение и проверка вызова
def test_get_rate_uses_api(mocker):
mock_api = mocker.patch("myapp.rates.fetch_from_api")
mock_api.return_value = {"usd": 92.5} # что вернёт заглушка
rate = get_usd_rate()
assert rate == 92.5
mock_api.assert_called_once() # проверяем, что сходили в API один раз
Мок фиксирует контракт, а не реализацию. Если вы через assert_called_with проверяете каждый внутренний вызов, любой безобидный рефакторинг красит тесты в красный. Проверяйте наблюдаемый результат и факт обращения к внешней границе, а не пошаговую механику внутри.
Когда что выбирать
- Фикстура — когда нужна подготовка или уборка, и она повторяется больше одного раза. Один раз — оставьте в тесте, это честнее.
- Параметризация — когда тело теста идентично, а различаются только вход и ожидаемый результат.
- Мок — когда вызов пересекает границу вашего кода: сеть, файловая система, время, платный сервис. Свою чистую функцию мокать не нужно — тестируйте её напрямую.
Что ломается в реальных проектах
- Session-фикстура с изменяемым состоянием. Тесты проходят по одному, но падают вместе. Лечится сужением scope до
function. - Мок не там, где вызов. Тест зелёный, но реальный код никогда не проверялся. Всегда патчьте путь импорта в модуле-потребителе.
- Забытый
pytest.approx. Тест на дробные вычисления мигает: то проходит, то нет. - Взаимозависимые тесты. Один создаёт запись, другой на неё рассчитывает. Порядок тестов не гарантирован; каждый тест должен готовить своё состояние сам.
Что настроить дальше
Соберите фикстуры уровня проекта в корневой conftest.py и посмотрите, какие данные повторяются между тестами — это кандидаты на фабрики. Подключите pytest-cov, чтобы видеть, какие ветки кода тесты не задевают: часто «сто тестов» покрывают одну и ту же дорожку, а обработчик ошибок не трогают вовсе. Дальше стоит разобрать отдельно две темы, которые здесь не поместились: фабрики тестовых данных (например, через factory_boy) и фикстуры для асинхронного кода через pytest-asyncio.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: pytest documentation — Fixtures, pytest documentation — Parametrizing tests
Частые вопросы
У меня старая версия pytest, будет ли это работать?
Тест зелёный, но я подозреваю, что мок не сработал. Как убедиться?
Можно ли передать фикстуру в параметризованный тест?
Фикстура с scope=session пересоздаётся на каждый тест — почему?
Как замокать текущее время или datetime.now()?
Стоит ли мокать базу данных в тестах?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.