Проверка вызовов и side_effect
Проверяем взаимодействия: как и с чем вызвали мок, плюс side_effect для сложного поведения.
Проверка взаимодействия — это проверка не результата, а факта: что код вызвал зависимость нужное число раз и с нужными аргументами.
Два вопроса к моку
После того как тестируемый код отработал, мок умеет ответить на два главных вопроса: «тебя вообще вызывали?» и «с какими аргументами?». Для этого есть assert-методы самого мока:
mock.assert_called()— был вызван хотя бы раз.mock.assert_called_once()— ровно один раз.mock.assert_called_with(args)— последний вызов был с такими аргументами.mock.assert_called_once_with(args)— один раз и именно с такими аргументами.mock.assert_not_called()— не вызывался вовсе.
Пример: проверяем, что письмо отправлено
Функция регистрации должна отправить приветственное письмо. Реальную отправку мокаем — нам важно проверить, что её вызвали правильно, а не слать письмо на самом деле.
import unittest
from unittest.mock import Mock
def register(user_email, mailer):
# ...сохранили пользователя...
mailer.send(user_email, "Добро пожаловать!")
return True
class TestRegister(unittest.TestCase):
def test_sends_welcome_email(self):
mailer = Mock()
register("[email protected]", mailer)
# Проверяем взаимодействие, а не результат
mailer.send.assert_called_once_with("[email protected]", "Добро пожаловать!")
self.assertEqual(mailer.send.call_count, 1)
unittest.main(argv=[''], exit=False, verbosity=2)
Вывод:
test_sends_welcome_email (__main__.TestRegister.test_sends_welcome_email) ... ok ---------------------------------------------------------------------- Ran 1 test in 0.001s OK
Мы убедились: send вызван ровно один раз и с правильными адресом и текстом. Никакой почтовый сервер не задействован.
call_count и call_args_list
Когда мок вызывают несколько раз, полезны call_count (сколько) и call_args_list (с чем каждый раз). Объект call(...) описывает один вызов:
from unittest.mock import Mock, call
logger = Mock()
logger.info("старт")
logger.info("шаг 1")
logger.info("готово")
print("Всего вызовов:", logger.info.call_count)
print("Список вызовов:", logger.info.call_args_list)
# Проверяем, что был именно такой вызов
assert call("шаг 1") in logger.info.call_args_list
print("Вызов 'шаг 1' зафиксирован")
Вывод:
Всего вызовов: 3
Список вызовов: [call('старт'), call('шаг 1'), call('готово')]
Вызов 'шаг 1' зафиксирован
side_effect: разные ответы и исключения
return_value задаёт один и тот же ответ. side_effect — мощнее: им задают последовательность ответов, исключение или функцию, вычисляющую ответ по аргументам.
from unittest.mock import Mock
# 1) Список — мок отдаёт значения по очереди
counter = Mock(side_effect=[10, 20, 30])
print(counter(), counter(), counter())
# 2) Исключение — мок поднимет его при вызове
broken = Mock(side_effect=ValueError("сбой сети"))
try:
broken()
except ValueError as e:
print("Поймали:", e)
# 3) Функция — ответ зависит от аргумента
doubler = Mock(side_effect=lambda x: x * 2)
print(doubler(5), doubler(8))
Вывод:
10 20 30 Поймали: сбой сети 10 16
Вариант с исключением особенно ценен: так тестируют, что ваш код корректно обрабатывает падение зависимости (таймаут, ошибку API), не вызывая реальную ошибку.
Мокаем время и случайность через side_effect
С помощью patch и side_effect можно сымитировать «течение времени» — каждый вызов time.time() вернёт следующее значение:
import unittest
from unittest.mock import patch
import time
def measure(work):
start = time.time()
work()
return time.time() - start
class TestMeasure(unittest.TestCase):
@patch("time.time", side_effect=[100.0, 102.5]) # старт, затем финиш
def test_duration(self, mock_time):
elapsed = measure(lambda: None)
self.assertEqual(elapsed, 2.5)
unittest.main(argv=[''], exit=False, verbosity=2)
Вывод:
test_duration (__main__.TestMeasure.test_duration) ... ok ---------------------------------------------------------------------- Ran 1 test in 0.001s OK
Первый вызов time.time() вернул 100.0, второй — 102.5, поэтому измеренная длительность ровно 2.5 секунды — без всякого реального ожидания.
Итог
assert_called_withиassert_called_once_withпроверяют аргументы вызова.call_countиcall_args_list— сколько раз и с чем вызывали мок.side_effectзадаёт последовательность ответов, исключение или функцию.- Через
patch+side_effectмокают время и случайность детерминированно.