1. Mocking. Основы
В реальном промышленном бэкенде программный код практически никогда не функционирует в стерильном вакууме. На каждый полезный вычислительный такт процессора приходятся десятки обращений к внешнему миру: сетевые запросы к базе данных, отправка полезной нагрузки по HTTP в сторонние платежные шлюзы, синхронизация токенов в Redis или публикация событий в брокер сообщений Kafka.
Если в модульных (Unit) тестах задействовать реальные сетевые сокеты и внешние сервисы, тестовый набор неизбежно деградирует:
- Тесты станут мучительно медленными (миллисекунды сетевого I/O вместо наносекунд тактов CPU).
- Появится жесткая зависимость от окружения (требуется живой PostgreSQL и сетевой доступ).
- Тесты начнут «мигать» (flaky tests) из-за сетевых таймаутов, сторонних rate limits или исчерпания кредитов в API.
Мокирование (Mocking) — это инженерная практика изоляции тестируемого модуля путем подмены реальной внешней зависимости предсказуемым программным дублером (Test Double). В языке Go эта техника неразрывно переплетена с архитектурой интерфейсов и фундаментальным принципом инверсии зависимостей (Dependency Injection).
В этой главе мы разберем философию и механику создания дублеров, заглянем в устройство таблицы виртуальных методов itab, классифицируем виды тестовых двойников по классической таксономии и научимся избегать опасной болезни гипермокирования (Overmocking).
Зачем нужны моки в бэкенде?
Представьте классический сценарий регистрации нового пользователя: сервис обязан проверить уникальность адреса электронной почты в базе данных, сгенерировать токен активации и отправить приветственное письмо через внешний провайдер (например, SendGrid).
Без изолированных дублеров ваш тест превращается в интеграционную драму:
- Он требует предварительно поднятого инстанса
PostgreSQLс накаченными миграциями. - Он тратит реальные деньги за каждый вызов платного почтового API.
- Он падает с ошибкой в CI в тот момент, когда у провайдера почты происходят плановые технические работы.
Подменяя внешние системы моками, инженер конструирует изолированную «лабораторную среду». В ней хранилище данных мгновенно отвечает «Email свободен», а почтовый шлюз лишь подтверждает прием команды в памяти процесса, не выпуская ни одного сетевого пакета через сетевую карту.
Интерфейс — фундамент мокирования
В Go отсутствует традиционное для классических ООП-языков наследование классов, и рантайм языка принципиально не позволяет переопределять методы конкретной структуры в рантайме без низкоуровневых хаков памяти. Чтобы модуль поддавался чистому тестированию, он обязан опираться не на конкретный тип (например, *sql.DB), а на контракт поведения — интерфейс.
flowchart LR
Service["Business Logic<br/>(PaymentService)"] -->|"Зависит от"| Interface["interface Emailer<br/>(Контракт поведения)"]
Interface -.->|"Реализует"| RealClient["Production Client<br/>(Real Network I/O)"]
Interface -.->|"Реализует"| MockClient["Mock Client<br/>(In-memory records)"]
style Service fill:#1e1b4b,stroke:#38bdf8,stroke-width:2px,color:#bae6fd
style Interface fill:#1e1b4b,stroke:#f59e0b,stroke-width:2px,color:#fde68a
style RealClient fill:#1e1b4b,stroke:#ef4444,stroke-width:2px,color:#fca5a5
style MockClient fill:#064e3b,stroke:#10b981,stroke-width:2px,color:#6ee7b7
Основные типы дублеров (Test Doubles)
В повседневной разговорной речи разработчики привыкли называть «моком» любой тестовый заменитель. Однако в инженерной литературе (в частности, в классификации Мартина Фаулера и Джерарда Месароша) дублеры строго разделяются на категории. Понимание этой терминологии — обязательный признак инженерной зрелости:
- Dummy (Пустышка): Объект, который передается в метод исключительно для заполнения параметров вызова, но фактически никогда не используется внутри тестируемой ветки бизнес-логики.
- Stub (Заглушка): Простейший дублер, возвращающий жестко зашитый предопределенный ответ. Заглушка не валидирует входные параметры и не считает количество обращений к себе.
- Mock (Мок): Интеллектуальный дублер с запрограммированными ожиданиями (Expectations). Мок верифицирует протокол взаимодействия: он завершится ошибкой, если вы вызвали метод не дважды, а один раз, или передали некорректный идентификатор пользователя.
- Fake (Фейк): Полноценно функционирующая, но упрощенная реализация компонента, непригодная для продакшна. Яркий пример — легковесная
In-memory SQLiteвместо кластера PostgreSQL или ассоциативный массив в памяти вместо распределенного кэша Redis. - Spy (Шпион): Проксирующий дублер, протоколирующий все факты обращений к себе (число вызовов, переданные аргументы, временные метки), позволяя тесту верифицировать поведение постфактум.
Механика мокирования в Go: Ручной подход
Прежде чем прибегать к генераторам кода, каждый инженер обязан понимать устройство ручного дублера. В Go ручные моки пишутся предельно прямолинейно и прозрачно, не требуя ни строчки сторонних зависимостей.
// 1. Контракт доменного слоя
type Store interface {
GetBalance(ctx context.Context, userID int) (int, error)
}
// 2. Тестируемый бизнес-сервис
type PaymentService struct {
storage Store
}
// 3. Ручной тестовый дублер (Spy/Stub) в файле payment_test.go
type storeMock struct {
balance int
err error
calls int // Счетчик вызовов для шпионажа
}
func (m *storeMock) GetBalance(ctx context.Context, userID int) (int, error) {
m.calls++
return m.balance, m.err
}
func TestPayment(t *testing.T) {
// Конфигурируем состояние дублера
mock := &storeMock{balance: 100, err: nil}
svc := PaymentService{storage: mock}
// Исполняем бизнес-логику сценария
// ...
// Проверяем инвариант взаимодействия
if mock.calls != 1 {
t.Errorf("expected 1 call to Store, got %d", mock.calls)
}
}
Mechanical Sympathy: Почему моки могут замедлять тесты?
Хотя моки исключают дорогостоящие сетевые и дисковые задержки ядра ОС, их использование сопряжено с микроархитектурной платой на уровне процессора.
Когда моки — это зло? (Overmocking)
Самый коварный антипаттерн в архитектуре тестирования — попытка замокать абсолютно всё (Overmocking). Если 80% объема тестового файла занимает декларация ожиданий дублеров через On("Method").Return(...), а содержательная проверка сводится к паре строк, вы тестируете не поведение системы, а детали ее сиюминутной реализации.
Симптомы «отравления» моками:
- Хрупкость к рефакторингу: Вы переименовали приватную переменную или оптимизировали внутренний алгоритм, не меняя публичного контракта, а в проекте упало 50 тестов, потому что моки ожидали конкретную последовательность шагов.
- Нечитаемые простыни инициализации: Сценарий теста тонет в десятках строк вызовов
gomockили сторонних конфигураторов. - Мокирование собственных приватных пакетов: Разработчик начинает мокать соседние структуры внутри одного и того же пакета вместо тестирования их совместной работы.
Итог
- Мокирование изолирует бизнес-логику от недетерминизма операционной системы, сетевых сокетов и внешних распределенных сервисов.
- Архитектурный фундамент мокирования в Go — это неявная реализация интерфейсов и таблица методов
itab. - Четко дифференцируйте дублеры: Stubs поставляют фиксированные данные, Mocks контролируют протокол взаимодействий, а Fakes моделируют функциональное поведение в оперативной памяти.
- Избегайте ловушки Overmocking: тестируйте доменные инварианты и бизнес-результат, а не внутренние вызовы приватных методов.
Ручное кодирование моков идеально прозрачно, но становится утомительным, когда интерфейс разрастается. О том, как проектировать интерфейсы специально для тестирования и соблюдать принцип разделения контрактов, мы поговорим в следующей главе: 2. Интерфейсы как инструмент тестирования.