На ревью обнаружен mock-тест Go, который проверяет порядок вызовов, не являющийся частью контракта. Какой главный риск создаёт такая проверка?
Такая проверка создаёт хрупкость теста: он начинает зависеть от внутреннего порядка реализации, хотя внешний контракт этого не требует. Любой безопасный рефакторинг, меняющий последовательность независимых вызовов, может ошибочно выглядеть как поломка поведения.
Mocks появились как способ изолировать unit-тест от внешних зависимостей и проверять взаимодействие с ними без сети, базы данных или других медленных компонентов. На практике ранние и чрезмерно строгие варианты mock-тестов часто фиксировали не только обязательные параметры вызова, но и детали реализации.
Поэтому полезно отделять проверку действительно значимого протокола от проверки случайного порядка действий. Тест должен защищать контракт, а не сохранять конкретную структуру текущего кода.
Предположим, сервис обязан записать результат и отправить метрику, но эти операции независимы. Сегодня реализация выполняет запись, затем отправку метрики; завтра их порядок меняют ради оптимизации или удобства обработки ошибок.
Если mock требует строго прежнюю последовательность, тест упадёт без изменения наблюдаемого результата. Это увеличивает стоимость рефакторинга, провоцирует ослаблять тесты целиком и снижает доверие к сигналам CI.
Порядок вызовов следует проверять только тогда, когда он является частью контракта: например, сначала нужно открыть транзакцию, затем выполнить запрос, а после этого зафиксировать транзакцию. Если порядок не важен, проверяйте обязательные вызовы, их аргументы, количество и итоговое состояние зависимости, но не вводите глобальное требование последовательности.
Механизм проблемы прост: строгий mock хранит историю вызовов и сравнивает её с заранее заданной последовательностью. Такое сравнение превращает порядок операций в наблюдаемое требование теста, даже если production-код юридически или логически свободен этот порядок менять.
Минимальная иллюстрация хрупкой проверки:
Само наличие вызовов можно проверять без сравнения всей последовательности. Компромисс состоит в том, что менее строгий mock может пропустить ошибочный порядок, если порядок всё-таки важен; поэтому критерий должен исходить из контракта, а не из удобства конкретной реализации.
Для параллельных операций глобальный порядок особенно опасен: планировщик Go может менять фактическую последовательность, а mock начнёт превращать допустимое выполнение в нестабильный тест. В таких случаях лучше проверять независимые факты, а для обязательного отношения «сначала — потом» моделировать состояние или явно синхронизировать проверяемый протокол.
Сервис после сохранения заказа публиковал событие. В mock-тесте была зафиксирована последовательность Save, затем Publish, хотя контракт требовал лишь, чтобы событие не публиковалось при ошибке сохранения. После оптимизации команда начала выполнять независимую подготовку события до сохранения, и тесты стали падать.
Рассматривались три варианта. Сохранить строгий порядок было просто, но это оставляло хрупкость; полностью удалить проверки mock-а означало потерять контроль над важным условием; заменить проверку на условие «при ошибке Save Publish не вызывается» точнее отражало контракт.
Выбрали третий вариант: проверили аргументы, количество вызовов и отсутствие публикации при ошибке сохранения, но не фиксировали порядок там, где он не имел значения. В результате рефакторинг прошёл без ложных падений, а нарушение бизнес-условия по-прежнему обнаруживалось.
Она оправдана, если порядок влияет на корректность: управление транзакцией, протокол обмена сообщениями, последовательность состояний или обязательное условие безопасности. В таком случае порядок является частью контракта, и отказ от его проверки, наоборот, ослабит тест.
У параллельных операций может не существовать единственной допустимой глобальной последовательности. Планировщик выбирает порядок выполнения, поэтому тест, сравнивающий всю историю вызовов, способен стать нестабильным даже при корректном коде. Следует проверять множества вызовов, аргументы и необходимые отношения зависимости, а не случайную последовательность событий.
Нужно мысленно заменить внутренний алгоритм другой реализацией с тем же внешним поведением. Если новый алгоритм должен считаться корректным, но тест обязательно падает из-за порядка вызовов, тест зафиксировал деталь реализации. Такой тест следует сузить до обязательных взаимодействий или перенести проверку конкретного протокола на уровень, где этот протокол действительно является контрактом.