Почему тесты проходят локально, но падают в CI — 3 настоящие причины

Классика. Ты запускаешь тесты у себя — всё зелёное. Пушишь — и робот красит сборку в красный. На тех же тестах, на том же коде.
Первая мысль: «CI сломался». Почти всегда она неверная.
Вот что тут на самом деле происходит: CI — это первый честный запуск твоего проекта. Чистая машина, пустая папка, ничего лишнего. А на твоей за месяцы накопилось: установленные когда-то пакеты, забытые файлы, переменные окружения из прошлого проекта. Ты тестируешь не проект — ты тестируешь проект плюс свою машину.
Красный CI обычно значит: у любого другого человека твой код тоже не заработает. Разберём три причины, в таком порядке — от самой частой.
Причина 1: на твоей машине есть то, чего нет в CI
Это примерно половина всех случаев.
Типичный набор беглецов:
- Переменные окружения. У тебя лежит файл
.envс ключами. Он в.gitignore(и правильно). В CI его нет — тест лезет за ключом, получает пустоту и падает. Что это за файл и почему он не в репозитории — разбирали отдельно. - Файлы, которые не закоммичены. Тестовая картинка, дамп базы, конфиг, который ты создал руками полгода назад и забыл добавить в гит.
- Глобально установленные штуки. База данных, которая у тебя просто работает фоном. Утилита, которую ты ставил один раз на весь компьютер.
Как проверить. Самый надёжный способ — и он же самый недооценённый: клонируй свой репозиторий в новую пустую папку и запусти тесты там.
git clone <адрес-репозитория> /tmp/clean-check
cd /tmp/clean-check
npm ci && npm test
Если упало — поздравляю, ты воспроизвёл CI у себя за минуту и дальше чинишь с нормальной отладкой, а не через пуши.
Быстрый дополнительный взгляд — посмотреть, что вообще лежит рядом, но не в гите:
git status --ignored
Всё из этого списка, что нужно тестам, в CI отсутствует.
Как починить. Зависит от беглеца:
- Ключи — прописать в секретах CI, а в тестах не ходить в живые сервисы (подменяй ответы).
- Файлы — закоммитить, если это тестовые данные. Или создавать их в самом тесте, а не рассчитывать, что лежат.
- Сервисы — поднимать их в конфиге CI явно, а не надеяться, что «оно там есть».
Причина 2: у вас разные версии
Второй по частоте случай. Код один, а собрались два разных проекта.
Механика простая. У тебя пакеты установлены давно — ты их поставил и больше не трогал. CI ставит всё заново, с нуля, сегодня. И если в файле проекта версии записаны с «вилкой» (^4.17.1 значит «бери свежие мелкие обновления»), то сегодня приедет не то, что приехало полгода назад.
Плюс чисто человеческое: ты поставил пакет командой, но забыл закоммитить изменившиеся файлы проекта. У тебя он есть, в CI его нет.
Как проверить. Посмотри, какие версии реально стоят у тебя, и сравни с тем, что ставит робот в логе сборки. И проверь, что файл с фиксацией версий закоммичен:
git ls-files | grep lock
Если в выводе пусто — вот и причина. Файл package-lock.json (или его аналог) обязан лежать в репозитории.
Как починить. В CI ставь зависимости строго по зафиксированным версиям:
npm ci
Разница с привычным npm install принципиальная. npm ci требует файл с фиксацией, удаляет старую папку с зависимостями и ставит ровно то дерево, что записано. Он же откажется работать, если файл со списком и файл с фиксацией разошлись — и это хорошо: лучше честная ошибка сейчас, чем «у меня работает» потом. Отдельные симптомы этой рассинхронизации — почему падает установка зависимостей.
Причина 3: время, порядок и параллельность
Реже, но мучительнее: тест падает не всегда, а через раз. Три классических источника.
Часовой пояс. Машины в CI почти всегда живут в UTC, а ты — нет. Тест, который сравнивает дату с «сегодня», у тебя проходит, а в CI попадает во вчера. Проверка: запусти тесты руками в UTC.
TZ=UTC npm test
Починка: не бери текущее время из системы в тестах. Фиксируй дату явно — конкретным значением, а не «сейчас».
Порядок тестов. Один тест что-то оставил после себя — запись в базе, файл, изменённую глобальную настройку, — а следующий на это опирается. У тебя порядок один, в CI другой, и цепочка рвётся.
Проверка: прогони тесты по одному, в один поток (в Jest — npx jest --runInBand), и отдельно — запусти подозрительный файл в одиночку. Проходит в одиночку, падает в куче — диагноз поставлен.
Починка: каждый тест сам за собой убирает и не зависит от соседей. Скучно, но других вариантов нет.
Гонки. На твоей машине запрос успевает за 50 мс, а на загруженном раннере — за 800. Тест, который ждал «немножко», не дожидается. Признак — падает плавающе и в разных местах.
Починка: ждать не время, а событие. Вместо «подожди 300 мс» — «дождись, пока появится результат». Таймаут можно увеличить, но это лечит симптом, а не болезнь.
Если ничего не подошло
Действуй по порядку, не наугад:
- Прочитай лог сборки целиком, а не последнюю строку. Настоящая причина часто на двадцать строк выше — там, где ставились зависимости.
- Воспроизведи чисто (клон в пустую папку) — это отсекает половину гипотез за минуту.
- Дай модели весь лог, а не свой пересказ. Логи сборки длинные и скучные — это как раз то, что модели читают лучше нас.
- Не чини пушами. Двадцать коммитов «фикс CI» — признак, что шаг 2 пропущен.
И мысль напоследок. Красный CI на зелёных локальных тестах — это не поломка робота. Это бесплатный отчёт о том, что твой проект пока не переносится на чужую машину. Починив это один раз, ты чинишь и «у меня не запускается» для всех, кто придёт в проект потом. Зачем вообще нужен этот робот — разбирали здесь.
Может ли CI быть правда сломан?
Может, но это редкость: обновился образ раннера, отвалился внешний сервис, кончилось место. Признак — падают сборки, в которых код не менялся. Проверяется за минуту: перезапусти сборку на коммите, который раньше был зелёным.
Почему тест падает в CI через раз?
Это флаки-тест, и у него всегда есть причина из третьего пункта: время, порядок или гонка. Перезапускать до зелёного — плохая идея: так ты прячешь настоящий баг, который однажды выстрелит у пользователя.
Как повторить окружение CI у себя?
Проще всего — клонировать репозиторий в чистую папку и ставить зависимости строго по локу. Полное совпадение даст только тот же образ (через контейнер), но до этого доходит редко: чистый клон ловит большинство случаев.
Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.





