Фикстуры и моки в pytest
Поставил три фикстуры с разными областями жизни и три теста, которые их запрашивают. Результат: фикстура уровня теста создалась 3 раза, уровня модуля - 1 раз, уровня сессии - 1 раз. Одинаковый код, разница в одном слове, а цена на большом наборе - минуты против часов.
Стержень: фикстура - это подготовка с явно заданным временем жизни, а мок - подмена, которая обязана проверяться так же строго, как код.
// Формулировки: «какие бывают scope у фикстур?», «что подменять моками?», «чем опасны моки?»
Области жизни
Фикстура - функция, результат которой pytest подставляет тесту по имени аргумента. Область жизни говорит, как часто её создавать заново: на каждый тест, на модуль, на класс или на всю сессию.
Мой замер показывает разницу буквально: три теста получили три разных экземпляра фикстуры уровня теста и один общий - уровня модуля и сессии. Отсюда правило выбора: то, что дорого создавать и не меняется (соединение с базой, поднятый контейнер, загруженная модель), делают долгоживущим; то, что тест портит (данные, временные файлы), - на каждый тест.
// Смешение этих двух вещей и даёт классическую беду: тесты проходят по одному, но падают все вместе или в другом порядке. Причина всегда одна - общее состояние, которое один тест изменил, а другой на него рассчитывал. Проверяется это запуском в случайном порядке: если набор от этого краснеет, зависимость между тестами есть.
- фикстура
- подготовка, которую pytest подставляет тесту по имени аргумента
- область жизни
- как часто фикстура создаётся заново: на тест, модуль, класс или сессию
- зависимость между тестами
- тесты проходят по одному и падают вместе; ловится случайным порядком
Что подменять, а что нет
Подменяют то, что мешает тесту быть быстрым и предсказуемым: обращения к чужим сервисам, текущее время, случайность, отправку писем, платежи. Я проверил на случайности: без подмены можно проверить только «результат из списка», а с подменой - конкретное значение.
Не подменяют собственную бизнес-логику. Тест, где подменено всё, кроме одной строки, проверяет ровно эту строку и создаёт ложное ощущение покрытия. Такой тест пройдёт при любой поломке соседнего кода.
// Отдельно про место подмены. Подменять надо ТАМ, ГДЕ функция используется, а не там, где она объявлена: если модуль импортировал функцию себе, подмена в исходном модуле на него не подействует. Это самая частая ошибка при первом знакомстве с моками, и симптом у неё говорящий: подмена «не работает», хотя написана правильно.
- мок
- подмена внешней зависимости на управляемый объект
- место подмены
- подменять надо там, где функция используется, а не где объявлена
Чем моки опасны
Мок принимает всё. Я написал тест, который у подменённого объекта вызывает НЕСУЩЕСТВУЮЩИЙ метод, - тест зелёный. Это и есть главная опасность: мок не знает настоящего интерфейса, поэтому не заметит ни переименования метода, ни смены набора аргументов. Код сломан, тесты зелёные.
Лечится это двумя приёмами. Первый: создавать мок «по образцу» настоящего объекта - тогда обращение к несуществующему методу упадёт. Второй: проверять вместе с результатом ещё и то, КАК был вызван подменённый объект - с какими аргументами и сколько раз.
// И третий, самый надёжный: не подменять то, что можно поднять по-настоящему. Настоящая база в контейнере ловит расхождения диалекта и схемы, которых мок не поймает никогда. Цену за это я мерил отдельно, и она оказалась терпимой.
- мок по образцу
- подмена, знающая интерфейс оригинала; ловит переименования
- проверка вызова
- убедиться, что подменённый объект вызван с нужными аргументами
Как отвечать: «Чем опасны моки и что подменять не стоит?»
Опасны они тем, что принимают всё. Я специально писал тест, который вызывает у подменённого объекта несуществующий метод, - тест зелёный. Значит, мок не заметит ни переименования метода, ни смены набора аргументов: код сломан, а тесты проходят. Лечится это моком по образцу настоящего объекта - тогда обращение к несуществующему методу падает - и проверкой того, с какими аргументами подменённый объект был вызван, а не одного лишь результата. Что подменять стоит: чужие сервисы, время, случайность, отправку писем и платежи, то есть недетерминированное и внешнее. А что не стоит - собственную бизнес-логику: тест, где подменено всё кроме одной строки, проверяет только эту строку и создаёт ложное ощущение покрытия. И там, где можно поднять настоящее в контейнере, я предпочитаю поднять: настоящая база ловит расхождения диалекта и схемы, которых мок не поймает никогда.
Ответ показывает опасность воспроизведённым примером, даёт два лечения и проводит границу между внешним и своим. Предпочтение настоящей зависимости - зрелая позиция, а не догма про изоляцию.
На чём валятся
- −Подменяют функцию там, где она объявлена, а не там, где используется.
- −Создают мок без образца, и он молча принимает несуществующие вызовы.
- −Подменяют собственную логику и получают ложное покрытие.
- −Делают дорогую фикстуру на каждый тест и получают набор, который идёт полчаса.
- −Хранят состояние в фикстуре уровня сессии и получают зависимость между тестами.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.
- Фикстура написана через
yield. Что происходит с кодом после yield?A)Он не выполняется, ведь yield прерывает функциюB)Он выполняется как teardown после завершения тестаC)Он запускается только если тест упал, а на зелёном пропускаетсяD)Он выполняется до теста, а до yield — уже после его завершенияпоказать ответ и разбор
+B)Он выполняется как teardown после завершения теста// разбор: yield-фикстура отдаёт значение до yield (setup), а код после yield pytest выполняет как teardown по завершении теста — независимо от того, прошёл он или упал. Это встроенный аналог try/finally для ресурсов: открыть соединение до yield, закрыть после. Чистая замена связке setup/teardown.
- Зачем нужен файл conftest.py?A)Конфиг подключения к базе данных для продакшена приложенияB)Файл со списком тестов, которые нужно запускать в этом каталогеC)Это обязательная точка входа, без которой pytest не стартуетD)Держать общие фикстуры, видимые тестам без импорта
показать ответ и разбор
+D)Держать общие фикстуры, видимые тестам без импорта// разбор: conftest.py — место для общих фикстур, хуков и плагинов, которые pytest автоматически подхватывает для тестов в этом каталоге и ниже, без явного импорта. Так шарят клиент, сессию БД, фабрики между файлами тестов. Несколько conftest на разных уровнях дерева складываются по областям.
mock.patchне срабатывает: код всё равно зовёт настоящую функцию. Частая причина?A)Патч действует только внутри отдельного процесса тестового раннераB)Нужно перезапустить интерпретатор, чтобы патч вступил в силуC)mock.patch в принципе не умеет подменять функции, только классыD)Патчить надо там, где имя используется, а не где определенопоказать ответ и разбор
+D)Патчить надо там, где имя используется, а не где определено// разбор: Классическая ловушка: patch подменяет имя там, где оно ищется в момент вызова, а не там, где определено. Если модуль сделал
from mod import func, у него своя ссылкаmymodule.func— патчить нужно её (patch("mymodule.func")), а не patch("mod.func"). Отсюда правило «patch where it's used».- Чем удобен
monkeypatchпо сравнению с ручной подменой атрибута?A)Сам откатывает изменения после теста, без ручной очисткиB)Он делает подмену видимой сразу во всех параллельных процессахC)Он работает быстрее, потому что меняет объекты на уровне C-кодаD)Он умеет подменять только переменные окружения, но не атрибутыпоказать ответ и разбор
+A)Сам откатывает изменения после теста, без ручной очистки// разбор: monkeypatch — встроенная фикстура для временных подмен: setattr, setenv, setitem, chdir и т.п. Её ключевое удобство — автоматический откат всех изменений по завершении теста, поэтому не нужно вручную возвращать оригинал в finally. Меньше кода и нет риска «протёкшей» подмены в соседний тест.
- Фикстура с yield: что делает код после yield?A)Это teardown: выполняется после теста для очистки (закрыть, откатить)B)Выполняется до теста как дополнительная подготовка окружения перед запускомC)Возвращает второе значение в тест, который получает из фикстуры сразу два объектаD)Ничего: код после yield в фикстуре недостижим и не выполняется
показать ответ и разбор
+A)Это teardown: выполняется после теста для очистки (закрыть, откатить)// разбор: yield-фикстура моделирует ресурс с жизненным циклом: код до yield — подготовка (создать соединение, временную папку), значение отдаётся тесту, а код после yield — teardown, выполняемый по завершении теста (закрыть соединение, откатить транзакцию, удалить файлы). pytest гарантирует очистку даже при падении теста. Это чище связки setup/teardown из unittest.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.