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 моки в параметрах идут снизу вверх.
Проверьте себя
1. Что делает patch?
AУдаляет функцию навсегда
BВременно подменяет объект моком и восстанавливает оригинал после теста
CУскоряет тесты
DСчитает покрытие
2. Где нужно патчить объект?
AПо месту определения
BПо месту использования (там, где код его вызывает)
CВ любом месте, без разницы
DВ файле теста через import
3. При нескольких @patch в каком порядке моки приходят в параметры?
AСверху вниз
BСнизу вверх — ближайший к функции декоратор соответствует первому аргументу
CВ случайном порядке
DПо алфавиту
Поддержать проект