При настройке mock-зависимости обязательный вызов ни разу не происходит, но unit-тест остаётся зелёным. Какой этап проверки не выполнен?
Не выполнена проверка ожиданий mock-объекта. Само объявление обязательного вызова обычно только сохраняет ожидание; тест упадёт лишь тогда, когда фреймворк явно сверит фактические вызовы с ожидаемыми.
Mocks появились как разновидность тестовых двойников для проверки взаимодействия компонента с зависимостями. Это особенно важно, когда одного результата функции недостаточно: нужно убедиться, что зависимость была вызвана с нужными аргументами или что обязательное действие вообще произошло.
Такой подход решает проблему «зелёного» теста, который проверяет только итоговое значение и не замечает пропущенного шага во взаимодействии компонентов.
Если ожидание задано, но его проверка не запущена, тест может пройти даже при полном отсутствии вызова. В результате рефакторинг способен удалить важное взаимодействие, а тестовый набор этого не обнаружит.
Особенно опасна ситуация, когда mock-фреймворк обрабатывает неожиданные вызовы автоматически, но проверку пропущенных ожидаемых вызовов оставляет на тесте. Поведение зависит от конкретного фреймворка и способа его настройки.
После выполнения тестируемого кода нужно вызвать механизм проверки ожиданий: например, метод проверки mock-объекта или завершение контроллера, которое выполняет такую проверку. Надёжный тест должен гарантировать, что этот этап выполняется даже при раннем выходе из теста; для этого часто используют регистрацию проверки через очистку теста.
Минимальная схема без внешнего mock-фреймворка выглядит так:
Здесь Verify — не часть вызова зависимости, а отдельная фаза контроля. Если убрать persist(m), тест завершится ошибкой при выполнении очистки. В реальном фреймворке нужно проверить документацию: одни библиотеки требуют явного AssertExpectations, другие связывают проверку с контроллером и тестовым объектом.
Проверять следует только взаимодействия, являющиеся частью контракта компонента. Избыточные ожидания делают тест хрупким: изменение внутреннего порядка или числа вызовов может ломать тест без изменения внешнего поведения.
Сервис должен сохранять заказ после успешной валидации. В тесте был настроен обязательный вызов Save, но проверка ожиданий отсутствовала. После рефакторинга ветка сохранения перестала выполняться, однако тесты продолжили проходить.
Рассматривались три варианта. Проверять только возвращаемый результат было проще, но это не обнаруживало пропущенное сохранение. Проверять каждый вызов вручную давало контроль, но создавало повторяющийся код. Использовать встроенный механизм верификации mock-фреймворка было компактнее и единообразнее, но требовало правильно подключить его к завершению теста.
Выбрали третий вариант: ожидание объявлялось в месте подготовки, а проверка регистрировалась на завершение теста. В результате пропущенный вызов стал приводить к падению теста, а неявные внутренние вызовы без контрактной значимости не фиксировались.
Нет. Объявление ожидания обычно лишь записывает требование во внутреннее состояние mock-объекта. Нужен отдельный этап верификации, который сравнит это требование с фактически зарегистрированными вызовами. Исключение возможно, если конкретный фреймворк автоматически выполняет верификацию при завершении теста.
Пропущенный вызов означает, что тестируемый код не выполнил требуемое взаимодействие. Неожиданный вызов означает, что код обратился к зависимости способом, которого тест не разрешал. Оба случая нарушают контракт взаимодействия, но обнаруживаются на разных этапах: первый обычно при финальной проверке ожиданий, второй часто непосредственно во время вызова.
Если mock проверяется до завершения горутины, обязательный вызов ещё не успел произойти, и тест ложно сообщит о пропуске. Поэтому тест должен дождаться завершения асинхронной операции через канал, WaitGroup или другой явный механизм синхронизации, а затем выполнять верификацию. time.Sleep для этого ненадёжен: он не гарантирует завершение работы и увеличивает длительность теста.