В CI результат Go теста помечен как cached, хотя внешнее состояние системы уже изменилось. Какой механизм с...

В CI результат Go-теста помечен как cached, хотя внешнее состояние системы уже изменилось. Какой механизм сработал и почему такой тест может дать устаревший успешный результат?

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

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

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

Для принудительного нового выполнения обычно используют -count=1. Такой запуск отключает использование кэша тестовых результатов.

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

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

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

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

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

В результате код или внешняя система уже изменились, а Go возвращает ранее сохранённый результат. Особенно опасно, если CI воспринимает строку cached как подтверждение того, что тест действительно выполнился в текущем запуске.

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

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

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

Для диагностики важно отличать два вопроса:

  • был ли результат взят из кэша;
  • является ли сам тест детерминированным и изолированным.

Флаг -count=1 принудительно выполняет тесты заново. Это полезно для проверки подозрений на устаревший кэш, но не заменяет исправление теста: постоянное отключение кэша увеличивает время CI и скрывает проблему с неявными зависимостями.

Надёжнее сделать тест самодостаточным: поднимать контролируемые зависимости, явно задавать время и случайность, использовать временные каталоги и не полагаться на изменяемые внешние сервисы. Интеграционные проверки внешней системы следует выделять отдельно и запускать с осознанной политикой кэширования.

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

Тест клиента каталога проверял, что удалённый объект существует. После удаления объекта в тестовой среде CI продолжал показывать успешный результат с пометкой cached.

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

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

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

1. Можно ли считать любой повторный запуск с теми же исходниками кэшированным?

Нет. Кэширование зависит не только от исходников, но и от параметров запуска и условий, при которых Go считает тестовый запуск кэшируемым. Некоторые флаги или режимы запуска делают повторное использование результата невозможным.

Кроме того, одинаковый исходный код не гарантирует одинаковое поведение теста, если он зависит от внешних ресурсов, которые не являются надёжно отслеживаемыми входами. Поэтому кэш не доказывает детерминированность теста.

2. Почему случайное падение теста обычно не приводит к постоянному кэшированию ошибки?

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

Это не означает, что нестабильный тест безопасен: он может иногда успешно завершаться и тогда его успех будет использован из кэша. Для flaky-тестов нужно устранять источник недетерминизма, а не рассчитывать на особенности кэша.

3. Как проверить гипотезу, что CI не запускал тест заново?

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

Однако один свежий запуск не доказывает корректность. Следует повторить проверку в изолированной среде и явно контролировать внешние ресурсы, время, случайность и переменные окружения.