unittest.mock: 5 вопросов на собеседовании

Привет, Хабр!
Сегодня мы рассмотрим некоторые вопросы про unittest.mock, которые могут всплыть на собеседовании.
И перейдем сразу к вопросам.
Как замокать атрибуты класса и экземпляра?
Задача кажется простой: нужно временно подменить какое‑то значение в объекте. Но что делать, если это атрибут класса, а не экземпляра? Или если атрибут объявлен через @property
? Давайте разбираться.
Мок атрибута класса
Атрибуты класса можно замокать с помощью patch
. Важно понимать, что такие моки работают на уровне самого класса, а не конкретного объекта.
from unittest.mock import patch
class MyClass:
class_attribute = 'original'
with patch('__main__.MyClass.class_attribute', new='mocked'):
print(MyClass.class_attribute) # mocked
print(MyClass.class_attribute) # original
Почему так происходит? Потому что patch временно изменяет значение только внутри контекста. Как только with‑блок заканчивается, всё возвращается на круги своя.
Глубокий мокинг атрибута класса
Но что если нужно подменить атрибут класса глубже — например, если класс используется внутри другой структуры?
class OuterClass:
inner = MyClass()
with patch.object(OuterClass.inner, 'class_attribute', 'deep_mocked'):
print(OuterClass.inner.class_attribute) # deep_mocked
Это применимо, когда объект создаётся динамически, и нужно контролировать его поведение на разных уровнях вложенности.
Мок атрибута экземпляра
Тут немного сложнее. У экземпляра есть свой атрибут, и его можно подменять на ходу:
from unittest.mock import patch
class MyClass:
def __init__(self):
self.instance_attribute = 'original'
obj = MyClass()
with patch.object(obj, 'instance_attribute', 'mocked'):
print(obj.instance_attribute) # mocked
print(obj.instance_attribute) # original
Если хочется подменить все будущие экземпляры класса, а не только один объект, можно сделать вот так:
with patch.object(MyClass, 'instance_attribute', 'mocked', create=True):
obj1 = MyClass()
obj2 = MyClass()
print(obj1.instance_attribute, obj2.instance_attribute) # mocked mocked
Но если у объекта уже есть атрибут, create=True
не нужен.
Как работает side_effect?
Когда мы мокируем функции, чаще всего мы просто указываем return_value
, чтобы она всегда возвращала одно и то же. Это удобно, но иногда недостаточно. Что если поведение функции зависит от переданных аргументов? Или она должна выбрасывать исключения в определённых ситуациях? Или вообще каждый раз возвращать разное значение?
Вот тут и приходит на помощь side_effect
.
side_effect как список значений
Самый базовый вариант: даём моку список значений, и он будет возвращать их по порядку при каждом вызове.
from unittest.mock import Mock
mock = Mock(side_effect=[1, 2, 3, Exception("Больше нельзя!")])
print(mock()) # 1
print(mock()) # 2
print(mock()) # 3
try:
print(mock()) # Бросает исключение!
except Exception as e:
print(f"Ошибка: {e}")
Когда значения в списке заканчиваются, будет выброшено StopIteration. Это может быть полезно, когда мокируем итератор или API‑запрос, который возвращает разные результаты при каждом вызове.
side_effect как функция
Если хотим, чтобы мок динамически реагировал на переданные аргументы — даём side_effect
функцию.
from unittest.mock import mock_open, patch
mock = mock_open(read_data='file content')
with patch('builtins.open', mock):
assert read_file('dummy.txt') == 'file content'
Теперь мок ведёт себя как функция. Ещё пример — подмена API‑функции, которая должна возвращать разные результаты для разных входных данных:
def api_mock(endpoint):
responses = {
"/users": ["Alice", "Bob", "Charlie"],
"/status": "OK",
"/error": Exception("API недоступен"),
}
if endpoint in responses:
result = responses[endpoint]
if isinstance(result, Exception):
raise result
return result
return None
mock_api = Mock(side_effect=api_mock)
print(mock_api("/users")) # ["Alice", "Bob", "Charlie"]
print(mock_api("/status")) # "OK"
try:
print(mock_api("/error")) # Выбросит Exception
except Exception as e:
print(f"Ошибка: {e}")
side_effect для выбрасывания исключений
Иногда мок должен сломаться при вызове, например, если он эмулирует сеть, которая может упасть.
mock = Mock(side_effect=RuntimeError("Аварийное завершение!"))
try:
mock() # бах!
except RuntimeError as e:
print(f"Поймано исключение: {e}")
Это применимо, когда тестируем код, который должен обрабатывать ошибки:
def process_data(fetcher):
try:
data = fetcher()
return f"Данные получены: {data}"
except Exception as e:
return f"Ошибка: {e}"
mock = Mock(side_effect=ConnectionError("Нет сети"))
print(process_data(mock)) # Ошибка: Нет сети
Смешанный side_effect
Можно смешивать всё вышеописанное:
from unittest.mock import Mock
def crazy_mock(x):
if x < 0:
raise ValueError("Отрицательные числа нельзя!")
return x ** 2 # Возводим в квадрат
mock = Mock(side_effect=crazy_mock)
print(mock(2)) # 4
print(mock(5)) # 25
try:
print(mock(-1)) # Ошибка!
except ValueError as e:
print(f"Ошибка: {e}")
Здесь мок работает как функция, но в определённых случаях выбрасывает исключение.
Как проверить порядок вызовов мока?
В тестах бывает важно не просто проверить, что методы были вызваны, но и в каком порядке. Например, если мы тестируем логику оркестрации запросов в API‑клиенте, проверяем порядок вызова методов у объекта, или убеждаемся, что наш код не перемешивает вызовы.
В unittest.mock для этого есть несколько удобных инструментов:
mock_calls
— список всех вызовов моков в порядке выполненияassert_has_calls()
— проверяет, что вызовы были, но не следит за их порядкомassert_called_once_with()
— проверяет конкретный вызовcall()
— удобный способ описать ожидаемую последовательность вызовов
Разберём всё по порядку.
Проверяем полный порядок вызовов через mock_calls
Допустим, есть объект с двумя методами:
def read_file(file_path):
with open(file_path, 'r') as f:
return f.read()
Теперь проверяем, что методы вызваны в правильном порядке:
from unittest.mock import mock_open, patch
mock = mock_open(read_data='file content')
with patch('builtins.open', mock):
assert read_file('dummy.txt') == 'file content'
Если порядок вызовов был другим, тест упадёт.
Что если порядок вызовов не важен?
Иногда порядок не имеет значения, и важно лишь наличие самих вызовов. В таком случае используем assert_has_calls()
:
mock = Mock()
mock.method1()
mock.method2()
mock.method1()
expected_calls = [call.method1(), call.method2()]
mock.assert_has_calls(expected_calls) # Пройдёт, порядок не учитывается
Но если порядок важен, добавляем any_order=False
(по умолчанию он False):
mock.assert_has_calls(expected_calls, any_order=False) # Упадёт, если порядок не совпадает
Как проверить, что метод был вызван только один раз?
Если нужно убедиться, что метод не был вызван больше одного раза, используем assert_called_once_with()
:
mock = Mock()
mock.process_data(42)
mock.process_data.assert_called_once_with(42) # Пройдёт
mock.process_data(99) # Второй вызов
mock.process_data.assert_called_once_with(42) # Упадёт, метод вызывался дважды
Если метод мог вызываться несколько раз, но нам важен хотя бы один вызов с определёнными аргументами, используем assert_any_call()
:
mock = Mock()
mock.process_data(10)
mock.process_data(20)
mock.process_data(30)
mock.process_data.assert_any_call(20) # Был вызван с 20 хотя бы один раз
mock.process_data.assert_any_call(50) # Упадёт, 50 не передавали
Отслеживание вызовов у нескольких объектов
Допустим, есть два связанных объекта, и нужно проверить общую последовательность их вызовов:
mock1 = Mock()
mock2 = Mock()
mock1.method1()
mock2.method2()
mock1.method1()
expected_calls = [
call.method1(),
call.method2(),
call.method1()
]
assert mock1.mock_calls + mock2.mock_calls == expected_calls # Полный порядок вызовов
Как замокать контекстный менеджер?
Контекстные менеджеры (with open(...) as f)
встречаются повсеместно: работа с файлами, соединение с базой данных, управление сессиями API, и т. д. Если в коде используется with
, то мокать просто return_value
недостаточно — нужен правильный подход.
Почему обычный patch для open () не работает?
Допустим, есть функция, которая читает содержимое файла:
def read_file(file_path):
with open(file_path, "r") as f:
return f.read()
Наивная попытка замокать open()
не сработает:
from unittest.mock import patch
with patch("builtins.open", return_value="file content"):
print(read_file("dummy.txt")) # Не сработает!
Почему? Потому что open()
возвращает файловый объект, у которого есть методы .read()
, .write()
, .close()
, и просто подмена return_value
ничего не даёт.
Как правильно мокать open?
Используем mock_open()
, который создаёт корректный мок файла:
from unittest.mock import mock_open, patch
mock = mock_open(read_data="file content")
with patch("builtins.open", mock):
result = read_file("dummy.txt")
assert result == "file content" # Работает!
Теперь, когда функция вызывает open()
, она получает поддельный файловый объект, у которого .read()
уже заранее возвращает file content.
Мокаем write () для файлов
Допустим, есть код, который записывает данные в файл:
def write_file(file_path, data):
with open(file_path, "w") as f:
f.write(data)
Проверим, что данные действительно записались:
from unittest.mock import mock_open, patch
mock = mock_open()
with patch("builtins.open", mock):
write_file("dummy.txt", "hello world")
# Проверяем, что метод `.write()` вызывался с правильными данными
mock().write.assert_called_once_with("hello world")
Как замокать @property?
Свойства @propert
в Python — это не просто атрибуты, а дескрипторы. Их нельзя мокать обычным patch.object()
, потому что они вызываются как методы.
Рассмотрим задачу.
Почему @property нельзя замокать patch.object ()?
Допустим, есть класс:
def write_lines(file_path, lines):
with open(file_path, "w") as f:
for line in lines:
f.write(line + "\\n")
Мы хотим подменить my_property, но обычный patch.object()
не сработает:
mock = mock_open()
with patch("builtins.open", mock):
write_lines("dummy.txt", ["first", "second", "third"])
mock().write.assert_has_calls([
("first\\n",),
("second\\n",),
("third\\n",),
])
@property
— это не переменная, а метод с геттером. Его вызов идёт через get()
, а не через dict. Нельзя просто заменить его строкой «mocked», потому что это вызов метода.
Как правильно мокать @property?
Используем PropertyMock
:
from unittest.mock import patch, PropertyMock
class MyClass:
@property
def my_property(self):
return "original"
with patch("__main__.MyClass.my_property", new_callable=PropertyMock) as mock_prop:
mock_prop.return_value = "mocked"
obj = MyClass()
print(obj.my_property) # mocked
Теперь my_property
заменяется на мок, который можно настроить.
Как замокать @property, если он динамически вычисляется?
Допустим, есть свойство, которое вычисляет длину строки:
class MyClass:
def __init__(self, data):
self._data = data
@property
def length(self):
return len(self._data)
Хочется подменить length
, не трогая _data
. Используем PropertyMock
:
with patch("__main__.MyClass.length", new_callable=PropertyMock) as mock_prop:
mock_prop.return_value = 99
obj = MyClass("Hello")
print(obj.length) # 99, хотя "Hello" явно короче
Проверяем, что @property вызывался
Иногда хочется убедиться, что @property
реально использовался. У PropertyMock есть assert_called()
:
with patch("__main__.MyClass.length", new_callable=PropertyMock) as mock_prop:
obj = MyClass("test")
_ = obj.length # Запрашиваем свойство
mock_prop.assert_called_once() # Проверяем, что геттер вызывался
А какие интересные вопросы задавали вам? Пишите в комментариях.
Статья подготовлена для будущих студентов онлайн-курса «Python QA Engineer». Хорошая новость: в рамках этого курса студенты получат поддержку карьерного центра Otus. Узнать подробнее