При проектировании unit теста зависимость можно заменить mock или fake. Какой критерий выбора показывает, ч...

При проектировании unit-теста зависимость можно заменить mock или fake. Какой критерий выбора показывает, что fake будет уместнее?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Fake уместнее, когда тесту важно проверить поведение через состояние зависимости, а не факт и порядок отдельных вызовов. Он должен быть небольшой рабочей реализацией контракта, например хранилищем в памяти.

Mock выбирают, когда сам факт взаимодействия является частью проверяемого контракта: нужно убедиться, что зависимость вызвана с определёнными аргументами, количеством вызовов или результатом.

Исторический контекст

В тестовой практике появились разные способы изолировать код от внешних зависимостей. Mock проверяет взаимодействия через заранее заданные ожидания, а fake заменяет зависимость упрощённой, но реально работающей реализацией.

Такое разделение решает две разные задачи: контроль протокола общения компонентов и проверку поведения компонента в условиях, близких к реальным, но без стоимости настоящей базы данных, сети или файловой системы.

Постановка проблемы

Неправильный выбор делает тест либо хрупким, либо недостаточно информативным. Mock может заставить тест повторять внутренний алгоритм вызовов и ломаться после безопасного рефакторинга. Fake, наоборот, не покажет, что код забыл вызвать обязательный метод или передал неверный параметр.

Есть и отдельный риск: fake может отличаться от настоящей зависимости. Поэтому успешный unit-тест с fake не доказывает совместимость с реальной базой данных или внешним сервисом.

Подробное решение

Выбор следует делать по тому, что является контрактом тестируемого компонента:

  • если контрактом является результат работы со состоянием, подходит fake;
  • если контрактом является конкретное взаимодействие, подходит mock;
  • если нужно проверить совместимость с настоящей инфраструктурой, нужен интеграционный тест, а не только fake или mock.

Fake должен сохранять существенную семантику зависимости: например, повторный запрос должен видеть ранее сохранённые данные, отсутствующий ключ должен обрабатываться так же, как предусмотрено контрактом, а ошибки должны моделироваться явно. При этом fake не обязан копировать производительность, внутреннее устройство или все редкие свойства настоящей реализации.

Минимальный пример fake-хранилища:

package store import "testing" type Store interface { Put(int, int) error Get(int) (int, bool) } type MemoryStore struct{ data map[int]int } func (s *MemoryStore) Put(k, v int) error { s.data[k] = v; return nil } func (s *MemoryStore) Get(k int) (int, bool) { v, ok := s.data[k]; return v, ok } func TestMemoryStore(t *testing.T) { s := &MemoryStore{data: map[int]int{}} _ = s.Put(1, 42) if v, ok := s.Get(1); !ok || v != 42 { t.Fatal(v, ok) } }

Здесь тест проверяет семантику сохранения и чтения. Mock проверял бы, например, что метод Put вызван ровно один раз с нужными аргументами, но такая проверка была бы полезна только при наличии соответствующего требования в контракте.

Ситуация из практики

Сервис сохраняет заказ через интерфейс репозитория. Команда рассматривала три варианта. Mock был простым и быстро проверял вызов Save, но тесты фиксировали внутреннюю последовательность действий и начали ломаться после объединения двух операций. Fake на карте позволил проверять создание, чтение и обновление заказа через состояние, однако не выявлял ошибок SQL и особенностей транзакций. Прямое подключение теста к базе давало реалистичный результат, но делало unit-тесты медленными и зависимыми от окружения.

Выбрали fake для большинства unit-тестов сервиса, потому что его контрактом было корректное изменение состояния заказа. Для сценария, где обязательна публикация события после сохранения, оставили mock или специальный счётчик вызовов: факт публикации являлся частью контракта. Отдельно добавили интеграционные тесты с настоящей базой, чтобы проверять SQL, схему и транзакционное поведение.

Что кандидаты часто упускают

1. Может ли fake полностью заменить интеграционный тест?

Нет. Fake проверяет код против собственной модели поведения. Если в настоящей базе другая семантика уникальных ограничений, изоляции транзакций, сортировки или ошибок, fake этого не обнаружит. Интеграционный тест нужен для проверки границы между приложением и реальной инфраструктурой.

2. Почему слишком подробный mock делает unit-тест хрупким?

Он связывает тест с последовательностью внутренних шагов, которые могут не входить в публичный контракт. После рефакторинга результат и внешнее поведение остаются прежними, но количество или порядок вызовов меняются, и тест падает без реальной регрессии. Поэтому ожидания mock следует ограничивать существенными взаимодействиями.

3. Что делать, если fake упрощает важное поведение настоящей зависимости?

Нужно явно определить границу его применимости. Существенные правила следует реализовать в fake, а недостающие свойства покрыть интеграционными тестами; иногда вместо fake лучше использовать тестовый экземпляр реальной зависимости. Нельзя считать fake достоверным только потому, что его интерфейс совпадает с интерфейсом настоящего компонента.