Прямой ответ
Моки решают одну задачу — заменить внешнюю границу (сеть, БД, время, файловую систему) контролируемым объектом, чтобы тест проверял вашу логику, а не инфраструктуру. Главный принцип, который отличает полезные тесты от иллюзии покрытия: мокать чужое на границе, не свой код внутри — тест, который замокал половину тестируемого модуля, проверяет моки, а не программу. Инструменты: unittest.mock (Mock/MagicMock/patch) или pytest-обёртки (mocker из pytest-mock, monkeypatch для атрибутов/переменных окружения), а лучший способ уменьшить потребность в моках — внедрение зависимостей: объект-граница приходит параметром, и в тест подставляется фейк без patch-магии.
Базовый словарь mock
Mock — объект, отвечающий на любой вызов/атрибут (удобно и опасно: опечатка в имени метода не падает, а молча создаёт новый Mock); MagicMock — то же с поддержкой магических методов (__len__, итерация); spec=/autospec=True — Mock, ограниченный интерфейсом реального объекта (опечатка → ошибка: почти всегда включайте); return_value — что возвращает вызов; side_effect — последовательность возвратов, исключение или функция-обработчик (сценарии «первый вызов — таймаут, второй — успех»); assert_called_once_with(...) — проверка аргументов вызова. Для async-кода — AsyncMock (await-совместимый; обычный Mock в await-позиции — частая ошибка).
patch: где подменять — главный нюанс
patch("module.Target") подменяет имя в том модуле, где оно используется, а не где определено — классическая ошибка «запатчил requests.get, а тест ходит в сеть»: тестируемый модуль сделал from requests import get, и патчить надо mymodule.get. Правило чтения: patch-цель — путь импорта в тестируемом коде. Формы: декоратор (@patch(...) — аргументом в тест), контекст-менеджер (локально в блоке), patch.object (атрибут конкретного объекта), patch.dict (словари/os.environ — для переменных окружения). В pytest часто удобнее monkeypatch.setattr(...)/setenv(...) — тот же эффект с автоматическим откатом и без строк-путей (фикстуры — в статье про pytest). И дисциплина: патч с autospec=True ловит разъезд сигнатур — без него мок переживёт изменение реального API, и тест соврёт.
Что мокировать, а что нет
Мокается: сетевые вызовы (HTTP-клиент — целиком; лучше через подготовленные ответы, чем через заглушки низкоуровневых сокетов — клиенты и их подмены разобраны в статье про HTTP-клиенты); БД (для юнит-тестов — репозиторий-фейк, для интеграционных — реальная БД в testcontainers: гайд здесь); время (freezegun/time-machine вместо ручного patch datetime.now — надежнее); файловая система (tmp_path вместо моков open); случайность (фиксированный seed вместо мока random). Не мокается: собственный доменный код (тестируйте его настоящим — иначе рефакторинг ломает только тесты); простые структуры данных; то, что проще сконструировать, чем замокать (построить валидный объект модели дешевле, чем Mock с пятью return_value).
Антипаттерны, которые обесценивают тесты
Mock-лапша: тест из двадцати return_value, проверяющий «что вызвано» вместо «каков результат» — такие тесты ломаются при любом рефакторинге и не ловят реальные баги; проверяйте наблюдаемое поведение (возврат, состояние), а assert_called оставляйте для действительно важных взаимодействий («отправка письма произошла»). Мокирование своего кода: patch("myapp.services.helper") внутри теста на сервис — тест проходит при сломанном helper. Слишком глубокий патч: подмена внутренностей библиотек (socket, ssl) вместо их границ — хрупко и нечитаемо. Мок без spec: молча проглатывает опечатки и изменения API. Глобальные патчи на модуль без отката — «плывущие» тесты в сюите; контекст-менеджеры и фикстуры с автотеardown закрывают вопрос.
Фейки, стабы, моки: термины, полезные на практике
Строгая классификация xUnit-паттернов: dummy (заглушка-аргумент), stub (возвращает заготовленные ответы — «данные извне»), fake (упрощённая рабочая реализация — in-memory-репозиторий), mock (проверяет взаимодействия — «был ли вызван»). Практический вывод: большинство «моков» в вашем коде — стабы и фейки, и фейк (простой in-memory-репозиторий) почти всегда лучше патча: он переживает рефакторинг внутренностей и читается как обычный код. Mock с assert_called_* оставляйте там, где само взаимодействие — контракт (отправка уведомления, аудит-запись).
Практический сценарий: тест сервиса с внешним API
Сервис OrderService.confirm(order_id) читает заказ из репозитория, проверяет оплату через внешний платёжный API и отправляет уведомление через SMTP — три границы, три разные техники изоляции. Репозиторий — фейк: in-memory реализация протокола OrderRepository (протокол typing'ом, подмена через конструктор сервиса — DI, никакого patch; фейк переживёт любой рефакторинг внутренностей). Платёжный API — стаб через границу клиента: мокается PaymentClient.get_status (именно в модуле сервиса — правило «патчим где используется»), return_value = PaymentStatus.PAID, отдельным тестом — side_effect = TimeoutError (ветка ретрая проверяется без единого реального запроса). SMTP — mocker.patch("smtplib.SMTP") с assert'ом, что письмо ушло с ожидаемой темой (взаимодействие здесь и есть контракт — assert_called уместен). Что НЕ мокается: сам OrderService, доменные объекты, валидация — они настоящие во всех тестах. Итог: четыре теста (успех, не оплачен, таймаут с ретраем, ошибка уведомления) исполняются за миллисекунды, покрывают все ветки и не зависят ни от чего внешнего; при этом ни один тест не проверяет «как устроен сервис внутри» — рефакторинг реализаций тесты переживут, изменение контрактов границ — нет, что и задумано.
FAQ
unittest.mock или pytest-mock?
pytest-mock (mocker.patch, mocker.AsyncMock) — тонкая обёртка: короче синтаксис, интеграция с фикстурами, авто-откат. Под капотом тот же unittest.mock — знания переносимы.
Почему «запатчил, а всё равно ходит в сеть»? Две причины в 95% случаев: патч не того имени (см. правило «где используется») или объект-клиент создан до патча (создаётся на импорте/старте — патчите раньше или внедряйте зависимость).
Нужны ли моки в интеграционных тестах? Минимально: внешние платные/нестабильные сервисы — да (или записанные ответы, VCR); всё, что поднимается в docker (БД, редис) — лучше реальным. Интеграционный тест, где всё замокано, — это юнит-тест с лишними шагами.
Как мокировать время правильно?
freezegun.freeze_time("2026-10-20") или time-machine — фиксируют весь стек (datetime, time.time) согласованно. Ручной patch datetime.now в одном модуле оставляет остальные на реальном времени — источник плавающих тестов по ночам.
Источники: документация unittest.mock и monkeypatch (pytest), примеры mock — ссылки в шапке.