Прямой ответ
Тестовый двойник — объект, который в тесте заменяет реальную зависимость. Термин ввёл Джерард Месарош, а классификацию популяризовал Мартин Фаулер. Видов пять, и путаница между ними — причина большинства споров о «правильном» мокировании.
| Вид | Назначение | Пример |
|---|---|---|
| Dummy | Заполнить аргумент, не используется | None или пустой объект в параметре |
| Stub | Возвращает заранее заданные ответы | Платёжный шлюз всегда отвечает «одобрено» |
| Spy | Записывает, как его вызывали | Логгер, фиксирующий отправленные письма |
| Mock | Проверяет, что вызовы произошли как ожидалось | «send_email вызван ровно один раз с таким-то адресом» |
| Fake | Рабочая упрощённая реализация | БД в памяти вместо PostgreSQL |
Главное различие — что проверяется:
- Stub и fake проверяют состояние: результат работы кода при заданном поведении зависимости.
- Mock проверяет взаимодействие: сам факт и параметры вызова.
# STUB: зависимость отвечает заранее заданным значением, проверяем состояние
def test_order_marked_paid(gateway):
gateway.approve_result = ApprovalResult(ok=True, code="00")
order = process_payment(order_id=7, gateway=gateway)
assert order.status == "paid" # проверка состояния
# MOCK: проверяем, что зависимость была вызвана определённым образом
def test_receipt_sent_once(mailer):
process_payment(order_id=7, gateway=fake_gateway, mailer=mailer)
mailer.send.assert_called_once_with(to="user@example.test", template="receipt")
Практическое следствие: тесты на взаимодействие привязываются к реализации. Если способ отправки письма изменится, тест упадёт, хотя поведение системы для пользователя не изменилось. Тесты на состояние устойчивее, но не всегда доступны.
В Python основной инструмент — unittest.mock, а для точечной подмены атрибутов — monkeypatch из pytest.
from unittest.mock import MagicMock, patch
def test_retries_on_timeout(client):
fake = MagicMock(side_effect=[Timeout, Timeout, Response(200)])
with patch("billing.http.get", fake):
assert fetch_invoice(12).status_code == 200
assert fake.call_count == 3 # проверка взаимодействия
Практика: когда подменять, а когда нет
1. Подменяйте то, что вам не принадлежит. Сторонние API, платёжные провайдеры, почтовые сервисы, очереди. Реальные вызовы в тестах дают медленные и нестабильные прогоны — см. flaky-тесты.
2. Не подменяйте то, что проверяете. Если тест подменяет функцию расчёта, он проверяет не расчёт. Распространённая ошибка — подмена собственного модуля, из-за которой тест проходит при сломанной логике.
3. Предпочитайте fake, когда он доступен. Реальный Redis в памяти, SQLite вместо PostgreSQL, локальный файловый каталог вместо S3. Fake ведёт себя похоже на оригинал, поэтому находит ошибки, которые stub не покажет. Для баз данных удобнее контейнеры — см. интеграционные тесты.
4. Осторожнее с проверкой взаимодействия. Утверждение «вызвано один раз с такими-то аргументами» уместно, когда вызов и есть наблюдаемый эффект: отправка письма, публикация события, списание денег. Для внутренних деталей это лишний coupling.
5. Не мокайте объекты, которые не проектировали для подмены. Подмена приватного метода чужого класса создаёт тест, который ломается при любом рефакторинге. Механика подмены — в статье про мок и patch.
6. Двойники и фабрики — разные инструменты. Двойник подменяет поведение, фабрика создаёт данные; они часто работают вместе — см. фабрики тестовых данных.
Рекомендации:
- Проектируйте код под подмену. Зависимость, переданная аргументом, подменяется легко; импортированная внутри функции — тяжело. Это аргумент в пользу явной передачи зависимостей.
- Один двойник на тест — ориентир. Тест с пятью подменами обычно проверяет не поведение, а последовательность вызовов.
- Используйте
autospec. Подмена с автоматической спецификацией проверяет сигнатуру и ловит вызовы с неверными аргументами. - Следите за уровнем. Большое число подмен в unit-тестах нормально; в интеграционных они означают, что тест перестал быть интеграционным — о распределении уровней в статье про пирамиду тестирования.
- Не проверяйте внутренности двойника. Утверждения о том, какие атрибуты были установлены у мока, почти всегда признак связанного с реализацией теста.
Практический сценарий: 60 тестов падали после безобидного рефакторинга
Контекст: после переименования внутреннего метода отправки уведомления упало 60 тестов. Поведение системы не изменилось.
- Диагностика. Все упавшие тесты содержали утверждения вида
notifier._dispatch.assert_called_once_with(...), то есть проверяли приватный способ доставки, а не факт уведомления. - Причина. Тесты писались как проверка взаимодействия с внутренней реализацией. Рефакторинг, не меняющий наблюдаемого поведения, сломал их все.
- Что сделали. (а) Для публичного эффекта «пользователь уведомлён» оставили проверку состояния: в тестовой почте появилось письмо с нужным шаблоном; (б) внутренние вызовы убрали из утверждений; (в) там, где важна была именно последовательность (повтор при сбое), оставили проверку через spy с явным комментарием, почему это существенно.
- Профилактика. Ввели правило код-ревью: утверждение о вызове приватного члена требует обоснования; по умолчанию проверяется состояние или публичный эффект.
- Результат. Следующий рефакторинг того же модуля не сломал ни одного теста; при этом регрессия в самом уведомлении была поймана тестом на состояние.
Вывод: выбор вида двойника определяет, к чему привязан тест. Проверка состояния переживает рефакторинг, проверка взаимодействия — нет. Это главный критерий при выборе между stub, fake и mock.
FAQ
Можно ли всегда использовать mock вместо stub?
Технически да: MagicMock умеет и возвращать значения, и записывать вызовы. Проблема не в возможности, а в том, что тест начинает проверять взаимодействие там, где достаточно состояния, и становится хрупким.
Что лучше: внедрение зависимости или patch?
Внедрение через аргумент или конструктор делает подмену явной и не зависит от путей импорта. patch удобен для быстрого точечного перехвата, но связывает тест с расположением символа в модуле.
Нужны ли двойники, если есть контейнеры? Контейнеры закрывают интеграционный уровень: реальная БД, брокер, кэш. Двойники остаются для unit-тестов, где скорость важнее реалистичности, и для внешних сервисов, которые нельзя поднять локально.
Как тестировать код, который много работает с внешними системами? Выделить тонкий слой адаптера и подменять его, а сам адаптер проверять отдельно — контрактными тестами или небольшим числом интеграционных. Это уменьшает число двойников в основной логике.
Источники: Martin Fowler (Mocks Aren't Stubs), Python unittest.mock docs, pytest docs (monkeypatch) — ссылки в шапке.