Урок курса
Monkeypatch и mock в pytest: когда что применять
Python для продвинутых: ООП, типизация и тестированиеТесту не нужна реальная сеть, случайное время или настройка чужого компьютера. В pytest fixture monkeypatch позволяет временно заменить переменную окружения, атрибут или элемент словаря. После завершения теста сделанная через fixture подмена отменяется.
Рабочий пример monkeypatch.setenv
Создайте в одной папке файл app.py:
import os
def read_mode():
return os.environ.get("APP_MODE", "production")
def fetch_price(product_id):
raise RuntimeError("В учебном примере сетевой клиент не настроен")
def total(product_id, quantity):
return fetch_price(product_id) * quantity
Рядом сохраните test_app.py:
import app
def test_mode_from_env(monkeypatch):
monkeypatch.setenv("APP_MODE", "test")
assert app.read_mode() == "test"
def test_default_mode(monkeypatch):
monkeypatch.delenv("APP_MODE", raising=False)
assert app.read_mode() == "production"
def test_total_without_network(monkeypatch):
monkeypatch.setattr(app, "fetch_price", lambda product_id: 150)
assert app.total("book", 2) == 300
Установите pytest в своё учебное окружение командой python -m pip install pytest, затем из этой папки выполните python -m pytest -q. Для этих трёх тестов ожидается 3 passed. Вызов total использует подмену вместо функции с исключением: реальный внешний сервис не нужен.
setenv меняет окружение, delenv удаляет настройку, а setattr заменяет имя функции внутри модуля. Не устанавливайте raising=False у setattr без причины: обычная проверка существования атрибута помогает заметить опечатку.
Mock: когда важно проверить вызов
monkeypatch удобно заменяет зависимость. Mock дополнительно запоминает обращения: аргументы, число вызовов и возвращаемое значение. Его можно использовать вместе с monkeypatch, а не выбирать только один из двух инструментов.
Допишите в test_app.py тест для той же функции total:
from unittest.mock import Mock
def test_total_calls_price_once(monkeypatch):
price = Mock(spec=app.fetch_price, return_value=150)
monkeypatch.setattr(app, "fetch_price", price)
assert app.total("book", 2) == 300
price.assert_called_once_with("book")
Теперь проверены и результат расчёта, и контракт обращения за ценой. return_value задаёт ответ; side_effect=TimeoutError("timeout") позволил бы воспроизвести сбой. Последний вариант нужен, только если ваша функция действительно должна обрабатывать такой сбой — мок не придумывает обработку за приложение.
spec ограничивает известные атрибуты mock-объекта, но не гарантирует проверку сигнатуры вызова функции. Для проверки аргументов при создании подмены используйте create_autospec(app.fetch_price, return_value=150) или patch(..., autospec=True).
Иногда проще передать обычную функцию или небольшой fake-объект. Мок полезен там, где сам вызов — часть контракта; для чистого преобразования данных обычно достаточно проверить возвращаемый результат.
Почему patch не сработал: подменяйте имя в месте использования
Допустим, prices.py определяет fetch_price, а checkout.py импортирует её так:
# checkout.py
from prices import fetch_price
def total(product_id, quantity):
return fetch_price(product_id) * quantity
После импорта у checkout есть своё имя fetch_price. Подмена prices.fetch_price не изменит уже импортированную ссылку. Правильный тест подменяет checkout.fetch_price:
from unittest.mock import patch
import checkout
def test_total():
with patch("checkout.fetch_price", autospec=True, return_value=150) as price:
assert checkout.total("book", 2) == 300
price.assert_called_once_with("book")
Если код использует import prices, а потом prices.fetch_price(...), место поиска имени другое: подменять нужно атрибут модуля prices. Сначала посмотрите выражение, которое выполняет приложение, затем выбирайте цель patch.
Ошибки, которые делают тесты хрупкими
- Подмена включена после вызова функции. Все подмены должны действовать до вызова проверяемого кода.
- Тест случайно ходит в реальную сеть. Подмените сетевую границу либо выделите такой тест в отдельную интеграционную проверку.
- Проверен только факт вызова, а результат не проверен. Добавьте assertion на внешнее поведение функции.
- Проверяется каждый внутренний вызов. При безопасном рефакторинге такой тест ломается без изменения поведения.
- Подмена создана вручную и не снята. Fixture
monkeypatchи контекстный менеджерpatchограничивают её время жизни.
Для продолжения разберите fixtures и их scope: подготовка тестовых данных и подмена зависимостей решают разные задачи.
Справка: monkeypatch в pytest, unittest.mock в Python.
Задача 1
Где следует patch-ить импортированную функцию?
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
