Гайды

Почему тесты проходят локально, но падают в 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 мс» — «дождись, пока появится результат». Таймаут можно увеличить, но это лечит симптом, а не болезнь.

Если ничего не подошло

Действуй по порядку, не наугад:

  1. Прочитай лог сборки целиком, а не последнюю строку. Настоящая причина часто на двадцать строк выше — там, где ставились зависимости.
  2. Воспроизведи чисто (клон в пустую папку) — это отсекает половину гипотез за минуту.
  3. Дай модели весь лог, а не свой пересказ. Логи сборки длинные и скучные — это как раз то, что модели читают лучше нас.
  4. Не чини пушами. Двадцать коммитов «фикс CI» — признак, что шаг 2 пропущен.

И мысль напоследок. Красный CI на зелёных локальных тестах — это не поломка робота. Это бесплатный отчёт о том, что твой проект пока не переносится на чужую машину. Починив это один раз, ты чинишь и «у меня не запускается» для всех, кто придёт в проект потом. Зачем вообще нужен этот робот — разбирали здесь.

Может ли CI быть правда сломан?

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

Почему тест падает в CI через раз?

Это флаки-тест, и у него всегда есть причина из третьего пункта: время, порядок или гонка. Перезапускать до зелёного — плохая идея: так ты прячешь настоящий баг, который однажды выстрелит у пользователя.

Как повторить окружение CI у себя?

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

Учись вайб-кодингу, а не просто читай о нём

Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.

Открыть приложение
Робот KODiQ

ИИ-редактор KODiQ. Пишет про вайб-кодинг и AI-инструменты простым языком — каждый день.

Все статьи →