Короткий ответ
Нужно передавать внешние зависимости (репозиторий БД, HTTP-клиент, почтовый отправитель) в конструктор или аргументы функции явно, а не создавать их внутри тела функции или импортировать как глобальный singleton, — тогда в тесте можно подставить тестовую реализацию или мок вместо реальной зависимости.
Как это работает подробнее
Если функция сама создаёт клиент БД или HTTP-сессию внутри себя (def process(): client = RealApiClient()), тест не может подменить эту зависимость без патчинга импортов, что более хрупко и неявно. Если зависимость передаётся параметром (def process(client): ...), тест просто передаёт тестовый двойник напрямую, без монкипатчинга модулей.
Пример кода
testing_example_322.py
# Плохо для тестирования: зависимость создаётся внутри функции
def send_report(user_id):
mailer = SmtpMailer() # нельзя подменить без патчинга модуля
mailer.send(user_id, "report ready")
# Лучше: зависимость передаётся явно
def send_report(user_id, mailer):
mailer.send(user_id, "report ready")
def test_send_report_calls_mailer():
fake_mailer = Mock()
send_report(user_id=1, mailer=fake_mailer)
fake_mailer.send.assert_called_once_with(1, "report ready")Что использовать на практике
- Практика. В Python для этого не обязателен сложный DI-фреймворк — обычно достаточно явных конструкторов классов, принимающих интерфейс/протокол зависимости, и фабричной функции или контейнера на уровне входной точки приложения (main, обработчик запроса), которая один раз собирает реальные зависимости. Это также упрощает переиспользование одной и той же бизнес-логики с разными реализациями хранилища (например, в тестах — in-memory, в проде — PostgreSQL).
Подводные камни
- Риск. Злоупотреблять patch() как заменой нормального проектирования — патчинг глубоко вложенных импортов работает, но делает тесты хрупкими к рефакторингу структуры модулей. Вводить DI-контейнер и избыточную абстракцию там, где у функции всего одна простая зависимость и явного параметра конструктора было бы достаточно.