


patch нужно применять там, где тестируемый код ищет объект. Если модуль импортировал функцию через from gateway import fetch_price, подменять следует имя checkout.fetch_price, используемое этим модулем. Подмена исходного имени в gateway после импорта не заменит уже сохранённую ссылку.
Разберём на Mentoring.digital учебную функцию расчёта суммы. Её задача: получить цену, умножить на количество и вернуть результат. Сетевой клиент в таком модульном тесте вызывать не требуется.
В файле gateway.py обозначим границу интеграции. Заглушка специально не делает сетевых запросов: пример можно запускать без ключей и аккаунтов.
# gateway.py
def fetch_price(sku):
raise NotImplementedError("External client is not configured")
# checkout.py
from gateway import fetch_price
def total(sku, quantity):
if quantity <= 0:
raise ValueError("quantity must be positive")
return fetch_price(sku) * quantity
Тест в test_checkout.py подменяет имя в checkout. Контекстный менеджер ограничивает время подмены одним блоком, затем исходный объект восстанавливается.
import unittest
from unittest.mock import patch
import checkout
class CheckoutTest(unittest.TestCase):
def test_total(self):
with patch("checkout.fetch_price", autospec=True) as price:
price.return_value = 120
self.assertEqual(checkout.total("BOOK", 2), 240)
price.assert_called_once_with("BOOK")
def test_error_is_propagated(self):
with patch("checkout.fetch_price", autospec=True) as price:
price.side_effect = TimeoutError("test timeout")
with self.assertRaises(TimeoutError):
checkout.total("BOOK", 2)
if __name__ == "__main__":
unittest.main()
Запуск: python -m unittest -v из папки с тремя файлами. Первый тест проверяет и сумму, и аргумент вызова. Второй фиксирует выбранный контракт: функция передаёт тайм-аут вызывающему коду. Если по требованиям нужен повторный запрос, тест и реализацию придётся изменить вместе.
autospec=True использует спецификацию заменяемого объекта. Для функции это позволяет выявить вызов с неправильной сигнатурой. Например, тест не должен незаметно принять лишний позиционный аргумент, которого настоящий клиент не поддерживает.
Но autospec не проверяет смысл ответа. Если настоящий API возвращает строку вместо числа, заранее настроенный return_value=120 этого не обнаружит. Нужна отдельная проверка адаптера на согласованном формате ответа и контролируемая интеграционная проверка.
Не подменяйте саму total: тогда расчёт останется непроверенным. Подменяйте границу, которую намеренно исключили из этого теста. Проверяйте наблюдаемое поведение, а не каждый внутренний вызов.
Добавьте случай количества 0: ожидайте ValueError и отсутствие вызова клиента через assert_not_called(). Затем временно измените множитель в реализации и убедитесь, что тест суммы падает. Так проще заметить проверку, которая вообще не связана с результатом.
Такие упражнения входят в практику на курсах Python. Выбрать разумные границы подмен в вашем проекте поможет ментор по Python и тестированию кода.