Автотести в 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, і тренди «цей тест мерехтить третій тиждень».
Практичний наслідок для AQA: якщо в UI пайплайна не видно, які тести впали, а видно лише червоний лог — майже завжди причина в тому, що ранер не згенерував JUnit XML або джоб його не підхопив. Це перше, що варто налаштувати на новому проєкті: без нього кожен фейл доводиться вичитувати руками з логів.
Перезапустити чи копати: рішення при червоному джобі
Кнопка «Re-run job» — найспокусливіша й найнебезпечніша в CI. Перезапуск — це діагностичний крок, а не полагодження. Він відповідає рівно на одне питання: тест падає стабільно чи мерехтить?
- Впало вдруге так само — це регресія: баг продукту або баг тесту. Копай.
- Стало зелено — тест флакі. І тут головна пастка: «зелено після перезапуску» відчувається як «полагоджено», хоча ти лише сховав нестабільність. Наступного разу вона стрельне в найгірший момент — на реліз-кандидаті.
Окрема тема — автоматичні ретраї (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; давній флак не тримають у блокувальних.
Джерела
- ISTQB Certified Tester Foundation Level Syllabus v4.0 — CI/CD і DevOps у життєвому циклі, роль автоматизації та зворотного зв'язку (розділ 2 «Testing Throughout the SDLC»).
- Playwright — Continuous Integration — запуск тестів у CI, headless за замовчуванням, конфігурація агента.
- Playwright — Trace Viewer — трейси як артефакт фейлу й перемотуваний розбір прогону.
- Playwright — Test reporters — репортери, зокрема JUnit XML, і збереження результатів для CI.
Чому один і той самий тест буває зелений локально й червоний у CI?
Бо це два різні середовища, а не одна машина в двох місцях. Автотест мовчки припускає купу речей про світ: розмір екрана, швидкість процесора, поточну таймзону, набір шрифтів, наявність залогіненої сесії. На твоїй машині ці припущення випадково справджуються, тому тест і проходить. CI-агент нічого з цього не гарантує: він стартує з чистого стану, часто в UTC, у headless, зі спільним CPU і мінімумом шрифтів. Кожне хибне припущення перетворюється на падіння. Тому «у мене працює» — це не аргумент про коректність тесту, а лише опис того, що у твоїх умовах він зелений.
Що таке headless-режим і як він може завалити тест, який проходить у тебе?
Headless — це запуск браузера без графічного вікна, дефолт для CI. Різниця не тільки косметична: інший дефолтний в'юпорт, немає апаратного GPU-рендера, іноді інакше поводяться діалоги й фокус. Класичний симптом — кнопка, яка на твоєму широкому екрані видима одразу, у headless опиняється нижче згину, і клік не доходить, бо тест не доскролив. Окрема пастка на Linux-агентах: там взагалі немає дисплея, і якщо тесту раптом потрібен headed-режим (наприклад, розширення браузера), він падає на старті, поки не піднімеш віртуальний дисплей типу xvfb.
Чому фіксовані паузи вважають головним ворогом стабільності саме в CI?
Бо waitForTimeout прив'язує очікування до годинника, а не до події. Локально в тебе вільні ядра, тож дія відпрацьовує за умовні 50 мс, і пауза в секунду має величезний запас. На спільному агенті поруч крутяться інші джоби, та сама дія займає 500 мс, і той самий запас зникає. Пауза, якої вистачало, раптом не вистачає — падіння виглядає випадковим, хоча воно детерміноване навантаженням. Збільшувати число безглуздо: ти або гальмуєш кожен прогін, або знову вгадуєш. Правильний хід — чекати на конкретну умову, а не на спливання таймера.
Що таке web-first assertions і чому вони стійкіші за фіксовану паузу?
Це асерти Playwright, які самі повторюють перевірку, поки умова не справдиться або не вийде таймаут. await expect(locator).toBeVisible() не питає стан один раз — він ретраїть його доти, доки елемент з'явиться, у межах відведеного часу. Різниця з паузою принципова: пауза чекає фіксовану кількість часу незалежно від того, готова сторінка чи ні, а web-first assertion завершується рівно тоді, коли настав потрібний стан. На швидкій машині він поверне майже миттєво, на завантаженому агенті дочекається — і той самий код стає стійким в обох середовищах без магічних чисел.
Як таймзона й локаль агента ламають тести, зелені на твоїй машині?
Через форматування й арифметику дат. Тест, що асертить дату у вигляді 18.07.2026 чи суму 1 234,50, зеленіє у твоїй локалі й червоніє на агенті в C/POSIX, де роздільники й порядок інші. Ще підступніший клас — тести на кшталт «сьогодні плюс один день»: вони можуть падати тільки вночі, коли на агенті в UTC уже настала наступна доба, а в тебе ще ні. Лікування — не підлаштовувати асерт під локальні дефолти, а робити час і локаль детермінованими: фіксувати таймзону тесту й мокати системний час там, де перевіряється логіка дат.
Скриншот-діф червоний, усі шукають зміну в CSS. Чому першим варто глянути на шрифти?
Бо в мінімальному контейнері потрібного шрифту може просто не бути. Тоді текст рендериться фолбеком, займає іншу ширину й висоту — і піксельне порівняння падає, хоча стилі ніхто не чіпав. Це типовий «виглядає як регресія UI, а насправді відсутній шрифт»: діф реальний, причина — оточення, а не код. Тому дивитися треба не лише на сам діф, а й на середовище, де він знявся: чи є в образі ті самі шрифти, що й локально. Рішення — доставити шрифти в контейнер або зафіксувати їх у базовому образі, щоб рендер був однаковий скрізь.
Що таке артефакти джоба і які з них ключові для дебагу автотестів?
Артефакти джоба — це файли, які пайплайн зберігає під час прогону й дає завантажити після нього. Вони потрібні, бо в CI прогін уже стався й зник: наживо ти його не бачив, лишився тільки лог. Три найкорисніші для автотестів: скриншот на момент падіння (найдешевший доказ — часто одразу видно перекриту банером кнопку чи 500 на сторінці), відео прогону (показує, як тест дійшов до фейлу) і трейс — покроковий запис із DOM-снапшотами, мережею й консоллю. Без них дебаг у CI перетворюється на вгадування за описом болю без знімка.
Чим трейс (Playwright Trace) потужніший за скриншот і відео?
Тим, що це перемотуваний запис прогону, а не одна картинка. Trace Viewer дає таймлайн кожної дії з DOM-снапшотами до й після неї, паралельно — мережеві запити, повідомлення консолі й логи. Ти відкриваєш його й опиняєшся рівно в тому стані сторінки, на якому тест спіткнувся, без жодного повторного запуску. Скриншот показує фінальний кадр, відео — рух, але тільки трейс дозволяє клікнути на конкретний крок і побачити реальний DOM, який бачив локатор, і що прилетіло по мережі саме тоді. Тому для нетривіального фейлу, який не відтворюється локально, трейс — головний інструмент.
Чому артефакти вмикають на падіннях і ретраях, а не на кожному прогоні?
Бо писати трейс, відео й скриншоти на весь прогін дорого: це місце й час на кожному тесті, навіть на зелених, яких переважна більшість. Тому їх вмикають вибірково — там, де вони справді потрібні. У Playwright це trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure': докази з'являються рівно на проблемних тестах, а зелені нічого не коштують. Ключове правило — увімкнути це до того, як стало червоно: коли джоб уже впав без налаштованих артефактів, доказів уже не буде, і лишиться дебажити наосліп.
Що таке JUnit XML і навіщо він потрібен пайплайну?
Це де-факто стандартний XML-формат, яким тест-ранер повідомляє CI структурований результат прогону. Для CI-системи джоб — просто процес, що завершився кодом 0 або 1; вона бачить червону галочку й лог, але не знає, які саме тести впали, скільки йшли й з якою помилкою. JUnit XML закриває цей розрив: корінь testsuites, усередині testsuite, у ньому testcase з атрибутами name, time і вкладеними failure/error/skipped. Єдиної офіційної специфікації в нього немає, тому в дрібницях інструменти розходяться, але базову структуру читають майже всі — Jenkins, CircleCI, GitLab CI, TeamCity. Ранер пише файл раз, а CI дістає з нього і список впалих у UI, і анотації в дифі PR, і тренди мерехтіння.
У пайплайні видно лише червоний лог, а списку впалих тестів немає. Де копати?
Майже завжди причина в тому, що JUnit XML не згенерувався або джоб його не підхопив. CI показує зручний список впалих, анотації в PR і тренди тільки тоді, коли ранер віддав йому структурований звіт; без цього файлу кожен фейл доводиться вичитувати руками з тисячі рядків логу. Тому перевіряють дві речі: чи ввімкнено JUnit-репортер у конфізі ранера, і чи зчитує пайплайн отриманий results.xml. Це одне з перших, що варто налаштувати на новому проєкті — інакше кожне падіння коштує ручного розбору логів.
Кнопка «Re-run job» — це полагодження чи діагностика?
Це діагностика, і плутати її з полагодженням небезпечно. Перезапуск відповідає рівно на одне питання: тест падає стабільно чи мерехтить. Якщо впало вдруге так само — це регресія, баг продукту або баг тесту, і треба копати. Якщо стало зелено — тест флакі, і тут головна пастка: «зелено після перезапуску» відчувається як «полагоджено», хоча ти лише сховав нестабільність. Вона нікуди не поділась і стрельне наступного разу — імовірно, у найгірший момент, на реліз-кандидаті. Тому перезапуск — це крок, після якого ти йдеш дивитись артефакти, а не закриваєш задачу.
Коли автоматичні ретраї легітимні, а коли перетворюються на маскування?
Різниця в намірі, а не в самому факті ретраю. Як пом'якшення ретраї виправдані: реальна мережа й реальні стенди інколи моргають, і глобальний прогін не має валитися через один випадковий збій. Але ретрай, який стабільно рятує той самий тест прогін за прогоном, — це вже не стабілізація, а приховування недетермінованості, борг, що тихо росте. Показник здоровий ретрай чи ні простий: якщо після врятованого ретраю ти йдеш дивитись трейс невдалої першої спроби — це інструмент; якщо просто радієш зеленому й забуваєш — маскування. Тому й трейс пишуть на першому ретраї: тест урятувався, а доказ, чому перша спроба впала, лишився.
Чи можна шукати причину флакі-тесту через git bisect?
Ні, на флакі-тесті бінарний пошук збреше. git bisect працює тільки тоді, коли падіння монотонне: до якогось коміту тест стабільно зелений, після нього — стабільно червоний, і кожен крок дає чесну відповідь «добрий/поганий». Мерехтливий тест ламає цю передумову: на «доброму» коміті він теж може випадково впасти, і bisect звинуватить не той коміт. Тому bisect доречний, коли треба знайти зміну, після якої тест став стабільно падати; для пошуку джерела флаку потрібні інші інструменти — трейс невдалої спроби й аналіз недетермінованості (гонки, спільні дані, час).
Пайплайн червоний — хто його чинить?
Той, чия зміна зробила його червоним, і робить це першим пріоритетом. Це принцип «зламав — лагодиш», він же «стоп-лінія»: поки спільна гілка червона, ніхто не будує зверху, бо новий код лягає на зламаний фундамент і вже не зрозуміло, чиє падіння ти зараз бачиш. Питання тут радше про культуру, ніж про техніку: команда, де червоне терплять як фонове тло, швидко звикає, що «червоне — це нормально», і пропускає справжню регресію. Червоний пайплайн — це сигнал, який або несе інформацію, або її знищує; терпіти його означає вбивати єдине, заради чого CI існує, — довіру до зеленого.
Спільна main стала червоною через свіжий мерж. Твоя першочергова дія?
Спершу повернути зелене, розслідувати потім. На trunk-based дефолт — швидкий git revert проблемного мержа: він знімає блок з усієї команди за хвилини, після чого автор спокійно шукає причину у своїй гілці, нікого не блокуючи. Логіка в тому, що червона спільна гілка коштує дорого — вона зупиняє всіх, хто мав би будувати зверху, — тож пріоритет не «зрозуміти, що сталось», а «розблокувати команду». Розслідування нікуди не тікає й робиться вже без тиску зупиненого конвеєра. Це протилежність спробі «дотиснути фікс прямо в main під червоним», яка тримає гілку зламаною ще довше.
Червоне через давній флакі-тест, не пов'язаний з твоєю зміною. Що з ним робити?
Не лишати його в блокувальних перевірках. Флак, який стабільно мерехтить у блокувальних, знецінює всю сигналізацію: команда звикає, що червоне буває «просто так», і перестає реагувати — а разом із шумом пропускає й справжню регресію. Тому такий тест ізолюють у карантин і чинять окремо, поза критичним шляхом, щоб він не блокував чужі PR. Що ганяти в блокувальних, а що винести в неблокувальний прогін, і як узагалі розкладати сьюти — це вже питання стратегії якості в пайплайні, але базовий інваріант простий: мерехтливий тест у блокувальних коштує дорожче, ніж користь, яку він дає.
«Тест проходить локально, а в CI падає. Твої дії?» Як відповідати на співбесіді?
Інтерв'юер перевіряє метод, а не знання команд. Слабка відповідь — «перезапущу, зазвичай допомагає»: вона показує рефлекс маскувати, а не розбиратись. Сильна йде системно. Спершу з'ясувати, стабільно тест падає чи мерехтить — це і є діагностична роль перезапуску. Потім відкрити артефакти: трейс, скриншот, відео з упалого джоба, щоб побачити реальний стан, а не гадати. Далі звірити дельту середовища — headless, ресурси й таймінги, час і локаль, шрифти, чистий стан і спільні дані. І тільки якщо фейл не відтворюється навіть з тим самим кодом, копати саме різницю середовищ. Головне — показати, що ти не кидаєшся тиснути Re-run, а рухаєшся від доказів.
Три кейси з життя AQA, де перемагає не швидкість реакції на червоне, а метод: як налаштувати докази ще до падіння, як за трейсом відрізнити флак від регресії й куди дивитися, коли фейл не відтворюється на твоїй машині.
Кейс 1. Робимо CI зрячим: конфіг артефактів + вивантаження в пайплайн
Найгірший стан — червоний джоб, у якому видно лише лог, бо трейс і скриншот ніхто не ввімкнув. Полагодити це заднім числом не можна: прогін уже зник разом з ефемерним агентом. Тому докази закладають у конфіг заздалегідь — вибірково, щоб не платити за них на кожному зеленому тесті.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
// на агенті даємо 2 ретраї як пом'якшення моргань, локально — жодного
retries: process.env.CI ? 2 : 0,
reporter: [
['list'],
['junit', { outputFile: 'results.xml' }], // структурований звіт для CI
['html', { open: 'never' }],
],
use: {
trace: 'on-first-retry', // перемотуваний розбір лише коли тест уже спіткнувся
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
});
Самі файли ще треба віддати пайплайну — інакше вони згинуть разом з агентом. У GitHub Actions це окремий крок, який спрацьовує навіть на червоному джобі:
# .github/workflows/e2e.yml
- name: Run Playwright tests
run: npx playwright test
- name: Upload artifacts
if: ${{ always() }} # без цього крок пропуститься на фейлі — тобто саме тоді, коли потрібен
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: |
playwright-report/
test-results/
results.xml
Що тут важливо і чому:
if: always()— не косметика. За замовчуванням наступні кроки пропускаються після впалого. Але артефакти потрібні саме на падінні, тож крок вивантаження має виконатися завжди, інакше на червоному джобі доказів не буде.trace: 'on-first-retry', а не'on'. Трейс на кожен тест роздуває час і місце. Прив'язка до ретраю дає запис рівно тих прогонів, що спіткнулись, — і якщо тест урятувався другою спробою, у тебе лишається доказ, чому впала перша.results.xmlу path. Без JUnit XML у вивантаженні CI покаже червону галочку, але не список впалих тестів у UI й не анотації в дифі PR.- CodeceptJS — та сама ідея інакше. Плагін
screenshotOnFailзнімає екран на кожному падінні, а під Playwright-хелпером доступні трейси й відео. Стек інший — принцип один: red-джоб без артефактів дебажать за описом болю без знімка.
Кейс 2. Тріаж червоного джоба: флак чи регресія
git push, і замість зеленої галочки — червоний checkout-flow.spec.ts. Локально він щойно пройшов десять разів. Спокуса натиснути Re-run величезна, але спершу треба зрозуміти, що цей клік насправді означає. Ось маршрут рішення.
| Крок | Питання | Що робиш |
|---|---|---|
| 1 | Стабільно падає чи мерехтить? | Re-run як діагностика, не як фікс |
| 2 | Впало вдруге так само | Регресія: баг продукту або тесту — копай |
| 3 | Стало зелено | Флак: не закривай задачу, іди до кроку 4 |
| 4 | Що показує трейс невдалої спроби? | Відкрий артефакт джоба, знайди крок фейлу |
| 5 | Відтворюється локально з тим самим кодом? | Так → звичайний дебаг; ні → дельта середовища |
Головна пастка ховається в кроці 3: «зелено після перезапуску» відчувається як «полагоджено», а насправді нестабільність лише відкладено на гірший момент. Тому навіть коли Re-run позеленів, дисципліна вимагає відкрити трейс першої спроби.
У Trace Viewer видно таймлайн: на кроці await expect(page.getByRole('alert')).toBeVisible() асерт впав за таймаутом, а на DOM-снапшоті банер із cookie ще перекриває сторінку. Дивимось код:
// Крихко: пауза встигає локально, під навантаженням агента — ні
await page.getByRole('button', { name: 'Оформити' }).click();
await page.waitForTimeout(1000);
await expect(page.getByRole('alert')).toBeVisible();
Пауза в секунду — це припущення «встигне за секунду», випадково істинне на твоєму вільному CPU й хибне на завантаженому агенті. Фікс не в тому, щоб підняти число, а в тому, щоб прибрати годинник із рівняння:
// Стійко: очікування прив'язане до умови, а не до таймера
await page.getByRole('button', { name: 'Оформити' }).click();
await expect(page.getByRole('alert')).toBeVisible({ timeout: 10_000 });
Тепер асерт сам ретраїться, поки алерт з'явиться: на швидкій машині поверне майже миттєво, на повільному агенті дочекається. Той самий код — стійкий в обох середовищах, без магічних чисел.
Кейс 3. «Падає лише вночі»: коли винна не логіка, а годинник
Тест renders due date тижнями зелений, а в нічному прогоні раптом червоніє й до ранку знову зеленіє сам собою. Артефакти показують дивне: очікували 19.07.2026, отримали 20.07.2026. Коду ніхто не змінював.
Розгадка — у дельті часу. Тест рахує «завтрашню» дату від системного годинника, а CI-агент живе в UTC. Коли в Києві ще 18 липня пізнього вечора, в UTC уже настало 19-те, і «сьогодні + 1» дає інший день. Локально о будь-якій порі твоя таймзона маскує проблему; на агенті вона проявляється лише в те вікно доби, коли дати по обидва боки від опівночі розходяться.
// Крихко: результат залежить від реального годинника й таймзони агента
test('renders due date', async ({ page }) => {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
await page.goto('/task/new');
await expect(page.getByTestId('due-date')).toHaveText(formatDate(tomorrow));
});
Лікування — прибрати недетермінованість, а не «полагодити асерт». Час фіксують, щоб і застосунок, і тест бачили одну й ту саму «зараз»:
// Стійко: заморожуємо системний час і таймзону — розбіжність опівночі зникає
test('renders due date', async ({ page }) => {
await page.clock.setFixedTime(new Date('2026-07-18T09:00:00'));
await page.goto('/task/new');
await expect(page.getByTestId('due-date')).toHaveText('19.07.2026');
});
Що варто винести з кейса:
- Симптом «падає тільки в певне вікно доби» — майже завжди про
UTCпроти локальної таймзони. Шукати баг у логіці задач марно: змінна — час прогону, а не код. - Форматування дат і сум — окрема гілка тієї ж проблеми. У
C-локалі роздільники й порядок інші, тож асерт на1 234,50чи18.07.2026червоніє від локалі агента, а не від зміни в продукті. - Фіксувати, а не підлаштовувати. Заморожений час і явна локаль роблять тест детермінованим; підгін асерту під
UTCлише переносить пастку на наступного, хто запустить сьюту в іншій зоні.
Дельта середовища: локально vs CI
- Розумію, що «на моїй машині працює» — не доказ коректності, а опис різниці двох середовищ (браузер, ресурси/таймінги, час/локаль, шрифти, стан, дані).
- Знаю, що CI за замовчуванням стартує headless, часто в
UTCіC/POSIX, з чистим станом; інший дефолтний в'юпорт ламає кліки по елементах «нижче згину». - Знаю, що на Linux-агенті немає дисплея, і headed-режим (наприклад, розширення) потребує віртуального дисплея типу
xvfb.
Таймінги і синхронізація
- Розумію, чому спільний агент (менше ядер + сусідні джоби) робить
waitForTimeoutлотереєю — тому хардкодні паузи головний ворог стабільності саме в CI. - Можу пояснити різницю: пауза чекає фіксований час незалежно від готовності, а web-first assertion ретраїть перевірку до появи стану — чекай на умову, не на годинник.
- Розумію, як
UTC/C-локаль ламає асерти на форматовані дати й суми, логіка «сьогодні + 1 день» падає лише вночі, а відсутній шрифт дає фолбек іншої ширини й червоний скриншот-діф.
Артефакти фейлу
- Розумію, навіщо потрібні артефакти: у CI прогін уже зник, тож тест мусить сам зберегти докази — скриншот (найдешевший), відео (як дійшло до фейлу), трейс (перемотуваний покроковий розбір).
- Можу пояснити, чому трейс потужніший за скриншот: DOM-снапшоти до/після дії, мережа й консоль без повторного запуску.
- Знаю, що докази вмикають вибірково й до падіння (
trace: 'on-first-retry',screenshot: 'only-on-failure',video: 'retain-on-failure') і ще треба віддати пайплайну (upload-artifact/artifacts:paths), інакше вони гинуть з ефемерним агентом.
JUnit XML
- Можу пояснити, чому ранер і CI говорять різними мовами: для CI джоб — це лише процес із кодом виходу 0 або 1.
- Знаю структуру JUnit XML (
testsuites→testsuite→testcaseзname,timeі вкладенимиfailure/error/skipped) і що єдиної офіційної специфікації немає, але базу читають усі. - Розумію, що один файл дає список впалих у UI, анотації в PR і тренди; немає списку впалих — майже завжди JUnit XML не згенеровано або джоб його не підхопив.
Перезапуск, ретраї, дебаг
- Розумію, що Re-run — це діагностика «стабільно чи мерехтить», а не полагодження; «зелено після перезапуску» ховає нестабільність до гіршого моменту.
- Знаю різницю між ретраєм-пом'якшенням (моргання мережі) і ретраєм-маскуванням (стабільно ховає той самий тест); трейс пишуть
on-first-retry, щоб лишити доказ невдалої спроби. - Знаю, що
git bisectбреше на флакі-тесті й працює лише на монотонному падінні.
Хто чинить червоне (культура)
- Можу сформулювати принцип «зламав — лагодиш» / «стоп-лінія»: чинить автор зміни, першим пріоритетом; поки спільна гілка червона, ніхто не будує зверху.
- Знаю розв'язку за типом червоного: власний PR — твоя зона (не зливай «поверх», не вимикай перевірку — для цього branch protection); спільна
main— швидкийgit revert, розслідувати потім. - Розумію, що давній флак не тримають у блокувальних (ізолюють у карантин), бо терпіти червоне як фон = втрачати довіру до зеленого.
Тест зелений локально, а в CI червоний. Що це насправді означає?
Питання
«На моїй машині працює» — що це насправді за твердження?