Автотести в 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. Єдиної офіційної специфікації в нього немає — формат виріс як домовленість, тому в дрібницях інструменти розходяться. Хто саме його читає, кожна CI-система документує сама про себе: у Jenkins є крок junit від JUnit-плагіна «for aggregating test reports», GitLab підтягує звіт секцією artifacts:reports:junit, Azure Pipelines — таском PublishTestResults@2 з testResultsFormat: 'JUnit'. Наскільки широко формат читають решта систем, джерела глави не кажуть — це перевіряють у доці своєї CI.
Цінність у тому, що це один формат для різних читачів. Ранер пише його раз — а 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; давній флак не тримають у блокувальних.
Джерела
Чому «локально зелено, у CI червоно»
- Playwright — Continuous Integration — дефолти чистого агента: браузери запускаються в headless, на Linux-агентах headed вимагає Xvfb; кількість ставиться за виявленою кількістю ядер, і перевищення дає непотрібні й падіння, тому в CI радять
workers: 1заради стабільності й відтворюваності. - GitHub Docs — Understanding GitHub Actions — кожен прогін іде на свіжій, щойно розгорнутій віртуальній машині, і один ранер виконує одну джобу за раз: накопиченого локального стану тут немає за побудовою.
- Playwright — Visual comparisons — рендеринг залежить від ОС, її версії, налаштувань, заліза й headless-режиму, звідси припис ганяти тести в тому самому середовищі, де знято .
Артефакти фейлу: як побачити те, чого ти не бачив
- Playwright — Trace Viewer — трейс як GUI-інструмент перегляду записаного прогону після його завершення: саме він заміняє «подивитися очима».
- GitHub Docs — Understanding GitHub Actions — чому без вивантаження доказів немає: кожна джоба біжить у власному ранері, який після завершення зникає.
- Playwright — Continuous Integration — вивантаження трейсів і звітів як артефактів прогону, разом із
DEBUG=pw:browserдля помилок запуску браузера.
JUnit XML: спільна мова тесту й пайплайна
- Playwright — Test reporters — репортер
junit«produces a JUnit-style xml report»; щоб писати у файл, потрібніPLAYWRIGHT_JUNIT_OUTPUT_NAMEі--reporter=junit. - Playwright — Continuous Integration — приклад споживання формату CI-системою: таск Azure Pipelines
PublishTestResults@2ізtestResultsFormat: 'JUnit'іtestResultsFiles: 'e2e-junit-results.xml'. - Jenkins Pipeline Documentation — крок
junitвід JUnit-плагіна «for aggregating test reports» у declarative-пайплайні. - GitLab Docs — CI/CD YAML syntax reference — ключ
artifacts:reports:junit, яким GitLab підтягує звіт джоби.
Перезапустити чи копати: рішення при червоному джобі
- Playwright — Continuous Integration — чому «перезапущу на тому самому агенті» не працює: перевищення воркерів над кількістю ядер — документована причина таймаутів і падінь, а не випадковість.
- Git Reference — git-bisect — бінарний пошук працює на монотонному падінні: із випадковим результатом веде пошук не в ту половину.
- Martin Fowler — Continuous Integration — «Continuous Integration can only work if the mainline is kept in a healthy state»; пріоритет дослівно словами Кента Бека — «nobody has a higher priority task than fixing the build», а найкращий перший крок — відкотити винний коміт із магістралі, щоб решта команди працювала далі.
- GitHub Docs — About protected branches — бік гейтів: обовʼязкова перевірка проходить у стані
successful, skipped, or neutral, а без обовʼязкових перевірок злити можна будь-коли, що збільшує ймовірність несумісних змін.
Чому один і той самий тест буває зелений локально й червоний у 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. Єдиної офіційної специфікації в нього немає, тому в дрібницях інструменти розходяться. Кожна CI-система документує власне читання цього формату сама: у Jenkins це крок junit від JUnit-плагіна, у GitLab — секція artifacts:reports:junit, в Azure Pipelines — таск PublishTestResults@2 з testResultsFormat: 'JUnit'. Наскільки широко формат читають інші системи, джерела глави не кажуть — це перевіряють у доці своєї CI. Ранер пише файл раз, а 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) і що єдиної офіційної специфікації немає, а підтверджені читачі документують себе самі: Jenkins (крокjunit), GitLab (artifacts:reports:junit), Azure Pipelines (PublishTestResults@2). - Розумію, що один файл дає список впалих у UI, анотації в PR і тренди; немає списку впалих — майже завжди JUnit XML не згенеровано або джоб його не підхопив.
Ретраї й дебаг
- Розумію, що Re-run — це діагностика «стабільно чи мерехтить», а не полагодження; «зелено після ретраю» ховає нестабільність до гіршого моменту.
- Знаю різницю між -пом'якшенням (моргання мережі) і ретраєм-маскуванням (стабільно ховає той самий тест); трейс пишуть
on-first-retry, щоб лишити доказ невдалої спроби. - Знаю, що
git bisectбреше на й працює лише на монотонному падінні.
Хто чинить червоне (культура)
- Можу сформулювати принцип «зламав — лагодиш» / «стоп-лінія»: чинить автор зміни, першим пріоритетом; поки спільна червона, ніхто не будує зверху.
- Знаю розв'язку за типом червоного: власний PR — твоя зона (не зливай «поверх», не вимикай перевірку — для цього branch protection); спільна
main— швидкийgit revert, розслідувати потім. - Розумію, що давній не тримають у блокувальних (ізолюють у ), бо терпіти червоне як фон = втрачати довіру до зеленого.
Квіз
Перед стартом
- Питань: 16
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
«На моїй машині працює» — що це насправді за твердження?