Урок курса

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-ить импортированную функцию?

Выберите один вариант
Без регистрации · результат не сохраняется

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку