vyvchy
    Теми розділу

    09 · Git і CI/CD

    Автотести в CI: стабільність і дебаг

    Зміст

    git push, чекаєш зелену галочку — а замість неї червоний джоб і лог на тисячу рядків. Локально той самий тест проходив десять разів поспіль. Перший рефлекс — «CI кривий, перезапущу». За п'ять хвилин джоб зелений, PR злитий, а через тиждень той самий тест валить реліз-кандидат. Ця глава — про те, як не потрапляти в цей цикл: чому CI бачить твій тест інакше, ніж твоя машина, і як дебажити прогін, за яким ти не спостерігав наживо.

    Це кут CI до великої теми флакі-тестів (flaky tests). Повна таксономія причин нестабільності, карантин і командна політика — канон у розділі про стратегію автоматизації; тут ми не переказуємо його, а розбираємо саме CI-дельту: чим середовище агента відрізняється від локального, як читати артефакти фейлу й де проходить межа між «перезапустити» і «знайти причину». Якщо ти вже читав канон флаку — це його CI-продовження.

    Чому «локально зелено, у CI червоно»

    Фраза «на моїй машині працює» — не доказ, а опис різниці двох середовищ. Автотест — це програма, яка робить приховані припущення про світ навколо: що екран певного розміру, що машина швидка, що зараз твоя таймзона, що шрифт відмальовується так само. Локально всі ці припущення випадково істинні. CI-агент їх не гарантує — і кожне хибне припущення стає падінням.

    АспектЛокальноCI-агент
    Режим браузераheaded, ти бачиш вікноheadless за замовчуванням
    Ресурситвої 8+ ядер, нічого поручкілька спільних ядер, поруч інші джоби
    Таймзона / локальтвоя (наприклад, Europe/Kyiv)часто UTC і C/POSIX
    Шрифтиповний набір ОСмінімальний, у контейнері часто майже порожній
    Станнакопичений кеш, залогінена сесіячистий, з нуля щоразу
    Данітвоя локальна базаспільний стенд, паралельні тести поруч

    Жодна з цих відмінностей не є багом CI. Це чесні дефолти чистого середовища — а падає тест, який мовчки розраховував на твої зручні умови.

    headless і чому він інший

    За замовчуванням у CI браузер запускається в headless-режимі — без графічного вікна. Історично headless-Chrome був окремою реалізацією і поводився помітно інакше за headed; сучасний Chromium звів обидва режими до спільної реалізації, але відмінності лишаються: інший дефолтний розмір в'юпорта, відсутній апаратний GPU-рендер, іноді інша поведінка діалогів і фокуса. Тест, який клікає по кнопці «нижче згину» на твоєму великому екрані, у headless з дефолтним в'юпортом може не доскролити до неї.

    На Linux-агентах ще й немає дисплея взагалі. Браузери Playwright у headless працюють без нього, але щойно тесту потрібен headed-режим (наприклад, розширення браузера) — потрібен віртуальний дисплей на кшталт xvfb. Це типова причина падіння «зелений локально, а в CI одразу помирає на старті браузера».

    Ресурси агента → інші таймінги

    Це головна CI-дельта флаку. Спільний агент має менше ядер, і поруч крутяться інші джоби. Те, що на твоїй машині відпрацьовує за 50 мілісекунд, під навантаженням займає 500. Будь-яке очікування, зашите в число замість умови, стає лотереєю: waitForTimeout(1000) вистачало локально й не вистачає під навантаженням.

    Тому хардкодні паузи (hard wait) — головний ворог стабільності саме в CI. Локально повільна машина маскується запасом, у CI запасу немає. Лікування — не збільшувати паузу, а чекати на конкретну умову: web-first assertions Playwright самі ретраяться до появи стану, а не до спливання таймера.

    // Крихко: локально встигає, у CI під навантаженням — ні
    await page.waitForTimeout(1000);
    await expect(page.getByRole('alert')).toBeVisible();
    
    // Стійко: очікування прив'язане до умови, а не до годинника
    await expect(page.getByRole('alert')).toBeVisible({ timeout: 10_000 });

    Механіку очікувань браузера детально розбирає глава «Практичні сценарії AQA: флак і синхронізація», а забутий await над асинхронним викликом як окреме джерело гонок — розділ про JavaScript/TypeScript.

    Окремий клас CI-специфічних падінь — час і локаль. Тест, що асертить відформатовану дату 18.07.2026 або суму 1 234,50, зеленіє у твоїй локалі й червоніє в UTC/C, де форматування інше. А тест, який рахує «сьогодні + 1 день», може падати лише вночі, коли на агенті в UTC уже завтра. І шрифти: у мінімальному контейнері потрібного шрифту немає, текст рендериться фолбеком, займає іншу ширину — ламається верстка або скриншот-порівняння. Про шрифти в контейнерах докладніше — «Docker і тестові середовища», про різницю оточень і конфігурацію під них — «Секрети та конфігурація в CI».

    Артефакти фейлу: як побачити те, чого ти не бачив

    Локально ти дебажиш очима: бачиш вікно, ставиш брейкпоінт, читаєш консоль. У CI прогін уже стався й зник — лишився тільки лог. Тому єдиний спосіб не дебажити наосліп — щоб тест сам зберіг докази свого падіння. Це артефакти джоба (job artifacts): файли, які пайплайн чіпляє до прогону, і які можна завантажити після нього.

    Ключові артефакти для дебагу автотестів:

    • Скриншот на момент падіння — найдешевший доказ. Часто одразу видно: модалка не закрилась, банер cookie перекрив кнопку, сторінка взагалі 500.
    • Відео прогону — показує, як тест дійшов до фейлу: що клікнув, що не встиг завантажитись.
    • Трейс (trace) — найпотужніше. Playwright Trace Viewer дає покроковий таймлайн з DOM-снапшотами до й після кожної дії, мережею, консоллю й логами. По суті це «запис перемотуваного прогону»: відкриваєш і бачиш саме той стан сторінки, на якому тест спіткнувся, без повторного запуску.

    Головне правило: артефакти треба ввімкнути до того, як стало червоно. Задавати їх на весь прогін дорого (місце, час), тому їх вмикають на падіннях і на ретраях:

    // playwright.config.ts
    export default defineConfig({
      retries: process.env.CI ? 2 : 0,
      reporter: [
        ['list'],
        ['junit', { outputFile: 'results.xml' }],
        ['html', { open: 'never' }],
      ],
      use: {
        trace: 'on-first-retry',      // трейс пишеться на першому ретраї
        screenshot: 'only-on-failure',
        video: 'retain-on-failure',
      },
    });

    Далі ці файли треба віддати пайплайну як артефакти джоба (крок upload-artifact у GitHub Actions, artifacts:paths у GitLab CI) — інакше вони помруть разом з ефемерним агентом. Механіку артефактів на рівні пайплайна розбирає «Будова пайплайна: стадії, джоби, тригери, артефакти».

    CodeceptJS дає те саме іншими словами: плагін screenshotOnFail зберігає скриншот на кожному падінні, а під Playwright-хелпером доступні трейси й відео. Ідея одна для будь-якого стека: red-джоб без артефактів — це дебаг за описом болю без знімка.

    JUnit XML: спільна мова тесту й пайплайна

    Твій ранер знає, що впало. CI-система — ні: для неї джоб це просто процес, що завершився кодом 0 або 1. Червона галочка й лог є, а структурованого «які саме тести впали, скільки часу йшли, з якою помилкою» — нема. Щоб пайплайн це зрозумів, потрібен спільний формат обміну. Де-факто стандарт тут — JUnit XML.

    Це простий XML родом з екосистеми JUnit (сам формат виріс із репортера Ant): корінь testsuites, усередині testsuite, у ньому testcase з атрибутами name, time й вкладеними failure/error/skipped. Єдиної офіційної специфікації в нього немає — формат виріс як домовленість, тому в дрібницях інструменти розходяться. Але базову структуру розуміють практично всі: Jenkins, GitLab CI, CircleCI, TeamCity.

    Цінність у тому, що це один формат для багатьох читачів. Ранер пише його раз — а CI дістає з нього і зручний список впалих у UI джоба, і анотації прямо в дифі PR, і тренди «цей тест мерехтить третій тиждень».

    пише

    читає

    Тест-ранер
    Playwright / CodeceptJS

    results.xml
    JUnit XML

    CI-система

    Список впалих
    у UI джоба

    Анотації в PR

    Тренди
    й історія

    пише

    читає

    Тест-ранер
    Playwright / CodeceptJS

    results.xml
    JUnit XML

    CI-система

    Список впалих
    у UI джоба

    Анотації в PR

    Тренди
    й історія

    Практичний наслідок для AQA: якщо в UI пайплайна не видно, які тести впали, а видно лише червоний лог — майже завжди причина в тому, що ранер не згенерував JUnit XML або джоб його не підхопив. Це перше, що варто налаштувати на новому проєкті: без нього кожен фейл доводиться вичитувати руками з логів.

    Перезапустити чи копати: рішення при червоному джобі

    Кнопка «Re-run job» — найспокусливіша й найнебезпечніша в CI. Перезапуск — це діагностичний крок, а не полагодження. Він відповідає рівно на одне питання: тест падає стабільно чи мерехтить?

    • Впало вдруге так само — це регресія: баг продукту або баг тесту. Копай.
    • Стало зелено — тест флакі. І тут головна пастка: «зелено після перезапуску» відчувається як «полагоджено», хоча ти лише сховав нестабільність. Наступного разу вона стрельне в найгірший момент — на реліз-кандидаті.

    Стабільно щоразу

    Раз падає, раз ні

    Так

    Ні

    Джоб червоний

    Падає стабільно
    чи мерехтить?

    Регресія:
    баг продукту або тесту

    Флак: копай причину,
    не перезапускай наосліп

    Відкрити trace / скриншот
    з артефактів джоба

    Відтворюється
    локально?

    Дебаж як звичайний баг

    Дельта середовища:
    headless, таймінги, час, локаль, дані

    Стабільно щоразу

    Раз падає, раз ні

    Так

    Ні

    Джоб червоний

    Падає стабільно
    чи мерехтить?

    Регресія:
    баг продукту або тесту

    Флак: копай причину,
    не перезапускай наосліп

    Відкрити trace / скриншот
    з артефактів джоба

    Відтворюється
    локально?

    Дебаж як звичайний баг

    Дельта середовища:
    headless, таймінги, час, локаль, дані

    Окрема тема — автоматичні ретраї (retries у конфізі). Вони легітимні як пом'якшення: реальна мережа й реальні стенди інколи моргають, і глобальний прогін не має падати через один випадковий збій. Але ретрай, який стабільно ховає той самий тест, — це не стабілізація, а маскування. Різниця в намірі: ретрай, після якого ти йдеш дивитись трейс невдалої спроби, — інструмент; ретрай, після якого ти забуваєш про тест, — борг, що росте. Тому трейс і варто писати саме on-first-retry: якщо тест урятувався ретраєм, у тебе лишається доказ, чому перша спроба впала.

    Коли фейл не відтворюється локально навіть з тим самим кодом — повертайся до таблиці дельти середовища. А коли треба знайти конкретний коміт, після якого тест позеленів у мерехтливий, допомагає git bisect — але лише якщо падіння монотонне; на флакі-тесті бінарний пошук збреше.

    Хто чинить червоний пайплайн

    Технічне питання «як стабілізувати» швидко впирається в організаційне: хто відповідає за червоне. Відповідь, що тримає команду живою, проста — той, чия зміна зробила пайплайн червоним, і робить це першим пріоритетом. Це принцип «зламав — лагодиш» (you break it, you fix it), він же «стоп-лінія» (stop-the-line): поки спільна гілка червона, ніхто не будує зверху, бо новий код лягає на зламаний фундамент і невідомо, чиє падіння тепер бачиш.

    Практична розв'язка залежить від того, що саме червоне:

    • Твій PR червоний — це твоя зона. Не зливай «поверх» червоного й не проси вимкнути перевірку. branch protection з обов'язковими статусами саме для цього й існує — розбирає «Pull Request і стратегії гілкування».
    • Спільна main червона через свіжий мерж — на trunk-based дефолт «спершу повернути зелене, розслідувати потім»: швидкий git revert знімає блок з усієї команди за хвилини, а автор спокійно шукає причину у своїй гілці.
    • Червоне через давній флакі-тест, не пов'язаний з твоєю зміною — його не можна лишати в блокувальних: він знецінює всю сигналізацію (команда звикає, що червоне — це «нормально», і пропускає справжню регресію). Такий тест ізолюють у карантин і чинять окремо. Що ганяти в блокувальних, а що ні, і як розкладати прогони — «Стратегія якості в пайплайні».

    Ключова думка: червоний пайплайн — не фонове тло, а сигнал, який або несе інформацію, або її знищує. Команда, де червоне терплять, швидко втрачає єдине, заради чого CI існує, — довіру до зеленого.

    Типові помилки

    Виглядає як «CI кривий», а насправді тест залежить від таймінгу. «Локально ж працює» — і винною призначають інфраструктуру. Насправді на швидкій машині приховане припущення «встигне за секунду» випадково істинне, а на завантаженому агенті — ні. Кривий не CI, крихкий тест.

    Виглядає як «полагодив», а насправді сховав. Перезапустив, стало зелено, закрив задачу. Нестабільність нікуди не поділась — ти лише відклав її на гірший момент. Зелено після ретраю без розбору причини — не результат, а відстрочка.

    Виглядає як регресія UI, а насправді відсутній шрифт. Скриншот-діф червоний, усі шукають зміну в стилях. А в контейнері просто немає потрібного шрифту — текст ліг фолбеком іншої ширини. Дивись не лише на діф, а й на оточення, де він знявся.

    Виглядає як «локально зелено — тест правильний», а насправді локальна машина маскує баг. Твій швидкий CPU, тепла сесія й зручна таймзона ховають гонку, яку CI чесно показує. Зелене локально доводить, що тест проходить у твоїх умовах, а не що він коректний.

    Виглядає як «джоб червоний без причини», а насправді немає артефактів. Дивишся в лог і не розумієш, що сталось, бо трейс і скриншот не ввімкнені або не завантажені як артефакт. Дебаг наосліп — не властивість CI, а наслідок неналаштованих доказів.

    Підсумок

    • CI — інше середовище, а не «та сама машина в хмарі». «У мене працює» не доказ: дельта — це ресурси й таймінги, headless, час і локаль, шрифти, чистий стан і спільні дані.
    • Головна CI-дельта флаку — таймінги: менше ядер і сусіди по агенту роблять хардкодні паузи лотереєю. Чекай на умову, а не на годинник.
    • Артефакти (трейс, скриншот, відео, JUnit XML) — твої очі в CI. Увімкни їх до падіння, інакше дебажиш за описом болю без знімка.
    • Перезапуск джоба — діагностика «стабільно чи мерехтить», а не лікування. Ретрай легітимний як пом'якшення й шкідливий як маскування.
    • Червоний спільний пайплайн — стоп-лінія: чинить той, чия зміна зламала; на trunk — швидкий revert, розслідування потім. Терпіти червоне = вбивати довіру до зеленого.

    Що питають на співбесіді

    «Тест проходить локально, а в CI падає. Твої дії?» Найчастіше питання теми. Інтерв'юер перевіряє не знання команд, а метод: чи не кинешся перезапускати, чи підеш системно — відтворити (перезапуск як діагностика), відкрити артефакти (трейс/скриншот), звірити дельту середовища (headless, ресурси, час, локаль, дані). Слабка відповідь — «перезапущу, зазвичай допомагає»; сильна — «спершу з'ясую, стабільно чи флакі, і подивлюсь трейс».

    «Що таке флакі-тест і як з ним боротися в CI?» Тут дивляться, чи відрізняєш маскування від лікування: ретрай і паузи ховають причину, а розв'язання — прибрати недетермінованість (очікування на умову, ізольовані дані, детермінований час). Згадка карантину й того, що флак у блокувальних убиває довіру до сьюти, — сильний сигнал.

    «Навіщо в CI трейси/відео/скриншоти?» Очікувана думка: у CI немає очей, прогін уже зник, тому тест мусить сам зберегти докази; артефакти вмикають на падіннях і ретраях, щоб не платити за них на кожному прогоні.

    «Що таке JUnit XML і навіщо він у пайплайні?» Перевіряють розуміння, що ранер і CI говорять різними мовами, а JUnit XML — спільний формат обміну: один файл дає список впалих, анотації в PR і тренди.

    «Пайплайн червоний — хто це чинить?» Питання про культуру, не про техніку. Правильний вектор: чинить автор зміни, першим пріоритетом; на спільній гілці — стоп-лінія й швидкий revert; давній флак не тримають у блокувальних.

    Джерела