Представьте сервис, чей mock полностью повторяет поведение реальной зависимости. Какой риск это создаёт для...

Представьте сервис, чей mock полностью повторяет поведение реальной зависимости. Какой риск это создаёт для достоверности unit-теста?

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

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

Такой mock может создать ложную уверенность: unit-тест проверит согласованность сервиса с моделью mock, но не совместимость с реальной зависимостью. Если mock воспроизводит её реализацию, он способен повторить ту же ошибку или не учесть важные детали протокола. Поэтому mock должен описывать небольшой внешний контракт, а совместимость с настоящей зависимостью следует проверять интеграционным или контрактным тестом.

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

Unit-тесты изолируют тестируемый код от базы данных, сети, файловой системы и других медленных или нестабильных компонентов. Mocks появились как способ управлять поведением зависимости и проверять реакцию тестируемого объекта на заданные сценарии.

Изоляция ускоряет обратную связь и делает сбои локальными, но одновременно убирает проверку взаимодействия с настоящей системой. Поэтому unit-тесты и интеграционные проверки решают разные задачи, а не являются взаимозаменяемыми подходами.

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

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

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

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

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

type Store interface { Get(id string) (string, error) } type Service struct{ store Store } func (s Service) Load(id string) (string, error) { return s.store.Get(id) } type fakeStore struct { value string err error } func (f fakeStore) Get(string) (string, error) { return f.value, f.err }

Здесь fake проверяет реакцию Service на результат контракта, но не пытается воспроизвести базу данных. Проверку SQL-запросов, сериализации, авторизации и реальных кодов ответа нужно вынести в интеграционные тесты или контрактные тесты на границе с зависимостью.

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

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

Сервис отправлял заказ во внешний HTTP-шлюз. Команда создала mock-клиент, который возвращал успешный ответ для любого тела запроса, и все unit-тесты проходили. После изменения шлюза выяснилось, что реальный API требует другой формат даты и отдельный заголовок идемпотентности.

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

В результате unit-тесты сохранили скорость и фокус на логике сервиса, а несовместимость с API стала обнаруживаться отдельным тестом. Главный компромисс — необходимость поддерживать дополнительный контрактный набор, зато граница ответственности тестов стала явной.

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

  1. Чем mock отличается от fake и stub в практическом смысле?

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

  1. Может ли контрактный тест заменить unit-тест с mock?

Нет. Контрактный тест проверяет совместимость компонентов и реальные форматы взаимодействия, но обычно медленнее, сложнее в настройке и хуже локализует ошибку в бизнес-логике. Unit-тест с mock быстро проверяет реакцию сервиса на конкретные результаты и ошибки. Эти виды тестов дополняют друг друга.

  1. Что делать, если реальная зависимость имеет слишком сложное поведение для простого mock?

Не следует автоматически переносить всю её реализацию в mock. Сначала нужно выделить минимальный контракт, существенный для тестируемого сервиса, затем покрыть его сценариями unit-тестов и отдельно проверить сложные детали интеграции. Если контракт нестабилен или плохо определён, это сигнал уточнить интерфейс и добавить интеграционные либо контрактные тесты, а не усложнять unit-тест до копии внешней системы.