patch: декоратор и контекст
Подменяем зависимости «на месте» с помощью patch — как декоратор и как контекст.
patch — инструмент, который временно заменяет объект в коде на мок, а после теста возвращает оригинал.
Зачем patch, если есть Mock
Создать мок легко. Сложнее — подсунуть его в тестируемый код вместо настоящей зависимости, особенно если код сам импортирует и вызывает её. patch решает именно это: на время теста он подменяет нужное имя (функцию, метод, класс) моком, а по выходе аккуратно всё восстанавливает.
Главное правило: патчим там, где используют
Патчить нужно объект по месту использования, а не по месту определения. Если ваш модуль делает import time и зовёт time.time(), вы патчите "time.time". В наших примерах код и тест в одном модуле, поэтому путь — "random.randint", "time.time" и т.п.
patch как декоратор
Декоратор @patch("путь") подменяет объект на время метода и передаёт мок первым аргументом после self. Имя параметра — любое, по соглашению с префиксом mock_.
import unittest
from unittest.mock import patch
import random
def lucky_roll():
# зависит от случайности — без патча тест невоспроизводим
return random.randint(1, 6)
class TestRoll(unittest.TestCase):
@patch("random.randint", return_value=4)
def test_roll(self, mock_randint):
result = lucky_roll()
self.assertEqual(result, 4) # теперь детерминированно
mock_randint.assert_called_once_with(1, 6)
unittest.main(argv=[''], exit=False, verbosity=2)
Вывод:
test_roll (__main__.TestRoll.test_roll) ... ok ---------------------------------------------------------------------- Ran 1 test in 0.001s OK
Внутри теста random.randint всегда возвращает 4 — функция стала предсказуемой. После теста настоящий random.randint возвращается на место автоматически.
patch как контекстный менеджер
Если подмена нужна не на весь метод, а на несколько строк — используют with patch(...) as mock:. Подмена действует только внутри блока.
import unittest
from unittest.mock import patch
import time
def make_log_line(message):
return f"[{int(time.time())}] {message}"
class TestLog(unittest.TestCase):
def test_log_line(self):
# Замораживаем время только внутри блока
with patch("time.time", return_value=1000.0):
line = make_log_line("старт")
self.assertEqual(line, "[1000] старт")
unittest.main(argv=[''], exit=False, verbosity=2)
Вывод:
test_log_line (__main__.TestLog.test_log_line) ... ok ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
Декоратор или контекст: что выбрать
| Форма | Когда удобнее |
@patch(...) декоратор | подмена нужна на весь тест |
with patch(...) контекст | подмена нужна на пару строк |
несколько @patch | несколько зависимостей сразу |
Несколько патчей сразу
Декораторы можно складывать стопкой. Важно: моки приходят в аргументы снизу вверх — ближайший к функции декоратор соответствует первому параметру.
import unittest
from unittest.mock import patch
import random, time
def event_id():
return f"{int(time.time())}-{random.randint(100, 999)}"
class TestEvent(unittest.TestCase):
@patch("random.randint", return_value=500)
@patch("time.time", return_value=42.0)
def test_id(self, mock_time, mock_rand): # снизу вверх: time, потом random
self.assertEqual(event_id(), "42-500")
unittest.main(argv=[''], exit=False, verbosity=2)
Вывод:
test_id (__main__.TestEvent.test_id) ... ok ---------------------------------------------------------------------- Ran 1 test in 0.001s OK
Самая частая ошибка: патч не туда
Девять из десяти проблем с patch — это патч по неправильному пути. Запомните принцип: патчить нужно имя там, где оно используется, а не там, где определено. Допустим, модуль orders.py делает from utils import now и зовёт now(). Внутри orders уже есть своя ссылка на функцию, поэтому патчить надо "orders.now", а не "utils.now" — иначе вы подмените оригинал, до которого orders уже не обращается, и тест не заметит подмены. Если патч «не срабатывает», первым делом проверьте путь.
patch.object и автоматический возврат
Кроме патча по строковому пути есть patch.object(obj, "method") — он подменяет атрибут конкретного объекта или класса. Это удобно, когда у вас уже есть ссылка на объект и не хочется указывать длинный путь строкой. В любом случае главное достоинство patch — он гарантированно восстанавливает оригинал после теста, даже если тест упал с исключением. Поэтому подмена никогда не «протекает» в другие тесты: каждый стартует с настоящими зависимостями. Это и делает патчинг безопасным.
Итог
patchвременно подменяет объект моком и затем восстанавливает оригинал.- Патчите по месту использования, а не определения.
- Декоратор передаёт мок аргументом; контекст ограничивает подмену блоком
with. - При нескольких
@patchмоки в параметрах идут снизу вверх.