Mocking: unittest.mock, patch, side_effect
В реальном коде функции зависят от внешних систем - HTTP API, БД, файлов, времени. Тестировать с реальными зависимостями медленно, ненадёжно, требует setup. Решение - mocking: заменить зависимости тестовыми двойниками которые ведут себя предсказуемо. В этом уроке - unittest.mock, patch, side_effect и pytest-mock, который отдаёт mock через фикстуру mocker.
Зачем mocking
# Без mocking - тест зависит от реального API
def test_get_weather():
weather = get_weather("Moscow") # реальный HTTP запрос!
assert weather["temp"] is not None
Проблемы:
- Медленно (network roundtrip)
- Нестабильно (API может быть недоступен)
- Зависит от внешнего state
- Невозможно тестировать error cases (HTTP 500)
- Не работает offline
С mocking:
def test_get_weather(mocker):
mocker.patch("myapp.requests.get", return_value=Mock(json=lambda: {"temp": 20}))
weather = get_weather("Moscow")
assert weather["temp"] == 20
Быстро, надёжно, изолировано.
Mock и MagicMock
from unittest.mock import Mock, MagicMock
m = Mock()
m.attribute = 42
m.method.return_value = "result"
print(m.attribute) # 42
print(m.method()) # result
print(m.anything) # автоматически создаётся Mock
print(m.deep.nested.attr) # тоже Mock
Mock автоматически создаёт attribute/method при доступе - не нужно объявлять заранее. Это «утиный» объект который притворяется чем угодно.
MagicMock - подкласс Mock с поддержкой magic methods (__len__, __iter__, __enter__, etc):
mm = MagicMock()
len(mm) # работает (Mock - не работало бы)
for x in mm: # работает - возвращает пустой iterator
...
with mm: # работает как context manager
...
В 99% случаев нужен MagicMock. Mock используется когда хочешь явно ограничить функциональность.
return_value vs side_effect
mock = Mock()
# return_value - возвращает одно и то же при каждом вызове
mock.return_value = 42
mock() # 42
mock() # 42
# side_effect - функция или iterable
mock.side_effect = [1, 2, 3] # последовательно
mock() # 1
mock() # 2
mock() # 3
mock() # StopIteration
# side_effect как функция
mock.side_effect = lambda x: x * 2
mock(5) # 10
mock(10) # 20
# side_effect как exception
mock.side_effect = ValueError("boom")
mock() # ValueError
side_effect мощнее return_value:
- Iterable - разные значения на разные вызовы
- Function - dynamic поведение
- Exception - моделирование errors
patch - подмена в коде
from unittest.mock import patch
# Подменим os.getenv в нашем коде на время теста
def get_config():
return os.getenv("DEBUG", "0")
def test_get_config():
with patch("os.getenv", return_value="1"):
assert get_config() == "1"
patch это context manager или декоратор. Подменяет указанный объект на Mock на время теста, автоматически восстанавливает.
patch для импортированных функций
Главное правило: патчи там, где используется, не где определена.
# myapp/services.py
from external import fetch_data
def process():
return fetch_data() + "_processed"
# tests/test_services.py
# ПРАВИЛЬНО - патчим в myapp.services где fetch_data импортирован
def test_process():
with patch("myapp.services.fetch_data", return_value="data"):
assert process() == "data_processed"
# НЕПРАВИЛЬНО - патчит в external, но myapp.services уже импортировал
def test_wrong():
with patch("external.fetch_data", return_value="data"):
assert process() == "data_processed" # FAIL - использует оригинал
Это связано с тем как Python обрабатывает импорты. После from external import fetch_data в myapp.services есть локальная reference. Патч external не затрагивает её.
patch как декоратор
@patch("myapp.services.fetch_data")
def test_process(mock_fetch):
mock_fetch.return_value = "data"
assert process() == "data_processed"
Mock объект передаётся в test как аргумент. Полезно для нескольких patches:
@patch("myapp.services.db")
@patch("myapp.services.cache")
def test_process(mock_cache, mock_db): # обратный порядок!
...
Порядок аргументов противоположен декораторам - bottom-up.
patch.object - патчим attribute класса
class API:
def fetch(self):
return real_api_call()
def test_api():
api = API()
with patch.object(api, "fetch", return_value="mocked"):
assert api.fetch() == "mocked"
Удобно когда не хочется указывать full path к атрибуту.
patch.dict - патчим словарь
import os
def test_env():
with patch.dict(os.environ, {"DEBUG": "1", "PORT": "8080"}):
assert os.environ["DEBUG"] == "1"
assert os.environ["PORT"] == "8080"
# После выхода - оригинал восстановлен
Лучше чем monkeypatch.setenv если нужно установить много значений сразу.
pytest-mock - удобная обёртка
pip install pytest-mock
Даёт fixture mocker:
def test_process(mocker):
mocker.patch("myapp.services.fetch_data", return_value="data")
assert process() == "data_processed"
Преимущества mocker над patch:
- Не нужен with statement или декоратор
- Cleanup автоматически (как у monkeypatch)
- Доступ ко всем patch методам:
mocker.patch.object,mocker.patch.dict
Этот стиль предпочтительнее в pytest проектах.
Проверка вызовов
Mock записывает информацию о вызовах:
mock = Mock()
mock(1, 2, key="value")
mock(3)
# Был ли вызван
mock.called # True
mock.call_count # 2
# Аргументы последнего вызова
mock.call_args # call(3)
mock.call_args.args # (3,)
# Все вызовы
mock.call_args_list # [call(1, 2, key='value'), call(3)]
# Assertions
mock.assert_called() # был вызван хоть раз
mock.assert_called_once() # ровно один раз
mock.assert_called_with(3) # последний вызов с этими args
mock.assert_called_once_with(3)
mock.assert_any_call(1, 2) # был ли где-то такой вызов
mock.assert_not_called()
Spec - ограничение mock интерфейса
class User:
def __init__(self, name):
self.name = name
def greet(self):
return f"Hi, {self.name}"
# Без spec - mock примет любой метод
mock_user = Mock()
mock_user.nonexistent() # работает (создаёт автоматом)
# С spec - только атрибуты класса
mock_user = Mock(spec=User)
mock_user.greet() # OK
mock_user.nonexistent() # AttributeError
spec=Class или spec_set=Class (более строгий) предотвращают опечатки и тесты которые проверяют несуществующие методы. Рекомендуется всегда использовать spec для mocking custom классов.
autospec - даже сигнатуры методов
def fetch(url, timeout=30):
...
with patch("myapp.fetch", autospec=True) as mock_fetch:
mock_fetch("https://api") # OK
mock_fetch("https://api", timeout=10)
mock_fetch(invalid_arg=True) # TypeError - нет такого аргумента
autospec проверяет сигнатуру при вызове - если вызов не совпадает с реальной функцией, ошибка. Защищает от incorrect mocking когда тест проходит но реальный код упадёт.
Mocking async functions
from unittest.mock import AsyncMock
async def fetch(url):
...
@pytest.mark.asyncio
async def test_async(mocker):
mock_fetch = mocker.patch("myapp.fetch", new=AsyncMock(return_value="data"))
result = await some_function_using_fetch()
assert result == "processed_data"
AsyncMock автоматически возвращает coroutine. Появился в Python 3.8 для тестирования async-кода.
Mock context manager
mock_file = MagicMock()
mock_file.__enter__ = Mock(return_value=mock_file)
mock_file.__exit__ = Mock(return_value=None)
mock_file.read.return_value = "file content"
with patch("builtins.open", return_value=mock_file):
with open("anything.txt") as f:
data = f.read()
assert data == "file content"
Или проще через mock_open:
from unittest.mock import mock_open
m = mock_open(read_data="file content")
with patch("builtins.open", m):
with open("anything.txt") as f:
data = f.read()
assert data == "file content"
Тестирование HTTP - responses / respx
Для HTTP лучше использовать специализированные библиотеки:
# для requests
import responses
@responses.activate
def test_api():
responses.add(responses.GET, "https://api.example.com/users",
json=[{"id": 1, "name": "Alice"}], status=200)
users = fetch_users()
assert len(users) == 1
# для httpx (async)
import respx
@respx.mock
def test_api():
respx.get("https://api.example.com/users").mock(
return_value=Response(200, json=[{"id": 1}])
)
users = await fetch_users()
Более expressive чем patch.requests.get и проверяют URL/method.
Когда mocking - анти-паттерн
1. Mocking чего-то простого
# Плохо - mock простой функции
def add(a, b):
return a + b
def test_user_total(mocker):
mocker.patch("myapp.add", return_value=10)
user = User(balance=5)
user.add_money(5)
assert user.balance == 10 # тест проверяет mock, не реальную логику
Mock'ай только то что дорого или внешнее (network, БД, время).
2. Mocking implementation, не behavior
# Плохо - тест ломается при refactoring
def test_save(mocker):
mocker.patch("user._validate")
mocker.patch("user._normalize")
mocker.patch("user._serialize")
user.save()
mocker.user._validate.assert_called()
Тестируй public API, не приватные методы.
3. Слишком сложные mocks
# Плохо - mock complex chain
mock = MagicMock()
mock.get_connection().cursor().execute().fetchall.return_value = ...
Это часто сигнал что тестируемый код плохо разделён - стоит refactor чтобы упростить тестирование.
Распространённые ошибки
1. Патч в wrong location
Уже обсуждали - патчи там, где используется.
2. Forget to verify mock was called
def test_send_email(mocker):
mock_smtp = mocker.patch("smtplib.SMTP")
send_welcome_email("alice@example.com")
# Забыли проверить что mock был вызван!
# Тест пройдёт даже если send_welcome_email ничего не делает
Всегда проверяй что mock реально вызвался:
mock_smtp.assert_called_once()
mock_smtp.return_value.sendmail.assert_called_once()
3. Mocking без spec
mock = Mock()
mock.deep_method_that_doesnt_exist() # работает молча
# Тест пройдёт, но в production AttributeError
Используй spec= для защиты:
mock = Mock(spec=RealClass)
4. Side effects в side_effect
def cleanup():
db.clean() # SIDE EFFECT в side_effect!
mock.side_effect = cleanup
Side_effect должен быть pure - либо exception, либо возвращать значение. Side effects делают тесты непредсказуемыми.
5. Не использовать pytest-mock в pytest
# Старый стиль
def test_x():
with patch("..."):
...
# pytest-style
def test_x(mocker):
mocker.patch("...")
...
mocker fixture проще и идиоматичнее в pytest проектах.
Хорошие практики
1. Mock на границе системы (boundary)
# Тестируем business logic, mocking external API
def test_user_registration(mocker):
mocker.patch("myapp.email_service.send")
user = register_user("alice@example.com")
assert user.status == "pending_verification"
2. Используй spec для безопасности
3. Verify вызовы Mock - используй assert_called_with
4. Предпочитай fakes/dependency injection для сложных случаев
# Вместо mock сложной БД
class FakeDB:
def __init__(self):
self.data = {}
def save(self, item):
self.data[item.id] = item
def get(self, id):
return self.data.get(id)
def test_with_fake_db():
db = FakeDB()
user = User("Alice")
db.save(user)
assert db.get(user.id).name == "Alice"
Fake это полноценная реализация для тестов - часто чище чем монолитный mock.
Сравнение с Go
В Go обычно используют interfaces для testability:
type DataSource interface {
Fetch() string
}
type RealAPI struct{}
func (r RealAPI) Fetch() string { return realCall() }
type MockAPI struct {
Response string
}
func (m MockAPI) Fetch() string { return m.Response }
func TestProcess(t *testing.T) {
mock := MockAPI{Response: "data"}
result := process(mock)
if result != "data_processed" {
t.Error("wrong result")
}
}
Go более явный - mocking через interfaces, не runtime patching. Python через unittest.mock более flexible но менее compile-time safe. Go стиль ближе к dependency injection с явными interfaces.
Мини-задание
- Mock простой функции:
import requests
from unittest.mock import patch
def fetch_user(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json()
# Тест
def test_fetch_user():
mock_response = type("Response", (), {"json": lambda self: {"id": 1, "name": "Alice"}})()
with patch("requests.get", return_value=mock_response):
user = fetch_user(1)
assert user["name"] == "Alice"
- side_effect для разных вызовов:
from unittest.mock import Mock
mock = Mock()
mock.side_effect = ["first", "second", "third"]
print(mock()) # first
print(mock()) # second
print(mock()) # third
# print(mock()) # StopIteration
- С pytest-mock и spec:
import pytest
class DataAPI:
def fetch(self, key):
...
def save(self, key, value):
...
def test_data_processor(mocker):
mock_api = mocker.Mock(spec=DataAPI)
mock_api.fetch.return_value = "data"
process_data(mock_api, "key1")
mock_api.fetch.assert_called_once_with("key1")
mock_api.save.assert_called_once_with("key1", "data_processed")
Что дальше
Освоили mocking. spec= особенно хорошо сочетается с Protocol: контракт описан явно, и mock проверяется по нему. В следующем уроке (последнем в Модуле 9) - линтеры и type checkers: ruff, black, mypy и pre-commit hooks для качества кода.