← Valtive QA Quiz quiz.valtive.io/notes/qaautomation valtive.io
📖 Про цей гайд📖 About this guide
Для когоWho it's for
Для автоматизаторів на Java або TypeScript, які готуються до співбесіди на AQA чи SDET. Гайд іде від стратегії й піраміди тестів через JUnit5/TestNG, Maven, Git і CI до інженерії самого фреймворку: шари й патерни, Allure, політика локаторів, ізоляція і тестові дані, API- і контрактні тести, техніки тест-дизайну в коді, flaky-тести, паралелізм і quality gates у пайплайні. Інструментальні деталі Selenium, Playwright чи RestAssured винесені в окремі гайди — тут фокус на тому, як усе це зібрати в надійну систему.
For Java or TypeScript automation engineers preparing for an AQA or SDET interview. The guide goes from strategy and the test pyramid through JUnit5/TestNG, Maven, Git and CI to engineering the framework itself: layers and patterns, Allure, locator policy, isolation and test data, API and contract tests, test design techniques in code, flaky tests, parallelism and quality gates in the pipeline. Tool-level detail on Selenium, Playwright or RestAssured lives in separate guides — here the focus is on assembling all of it into a reliable system.
Як користуватисьHow to use it
Фільтруй питання за рівнем або розділом. Відкрий картку, прочитай відповідь і розбери артефакт під нею: клас на Java, конфіг Playwright, YAML пайплайна, Dockerfile або файл налаштувань Allure. Спробуй відтворити його у своєму проєкті. Потім закрий текст і перекажи відповідь уголос за блоком 🗣 Як читати вголос — інтерв'юер чує, чи ти справді робив це руками, а не лише читав.
Filter the questions by level or by section. Open a card, read the answer and work through the artefact below it: a Java class, a Playwright config, a pipeline YAML, a Dockerfile or an Allure settings file. Try to reproduce it in your own project. Then hide the text and retell the answer out loud using the 🗣 How to say it out loud block — an interviewer can hear whether you have actually done this or only read about it.
Три якорі — з них випливає більшість відповідейThree anchors — most answers follow from them
ЯКІР 1 · Тест — це кодANCHOR 1 · Tests are code
До тестового коду ті самі вимоги, що й до продакшн-коду: шари, патерни, рев'ю, лінтери. Фреймворк, який важко читати, перестають підтримувати через пів року.
Test code gets the same demands as production code: layers, patterns, reviews, linters. A framework that is hard to read stops being maintained within six months.
ЯКІР 2 · ІзоляціяANCHOR 2 · Isolation
Кожен тест сам готує свої дані й стан і не залежить від порядку запуску. Лише тоді паралелізм, шардинг і ретраї працюють, а не множать flaky-падіння.
Every test prepares its own data and state and does not depend on run order. Only then do parallelism, sharding and retries work instead of multiplying flaky failures.
ЯКІР 3 · Сигнал у CIANCHOR 3 · Signal in CI
Автотести цінні настільки, наскільки команді можна довіряти червоному білду. Звіти, докази падіння, карантин і quality gates існують заради цієї довіри.
Automated tests are worth as much as the team can trust a red build. Reports, failure evidence, quarantine and quality gates all exist for that trust.
110
питаньquestions
3
рівніlevels
—
voice-блокиvoice blocks
12
розділівsections
📚 12 РОЗДІЛІВ📚 12 SECTIONS
—
Junior
—
Middle
—
Senior
0 / 110 · розгорни картку, щоб зарахувати їїopen a card to count it
▶ 1 10
1 Що таке піраміда тестування (Test Pyramid)? What is the Test Pyramid?
Концепції тестування та стратегія Testing concepts & strategy junior ▶

Що це — Піраміда тестування — це класична концептуальна модель у сфері забезпечення якості та розробки програмного забезпечення, яка визначає ідеальне співвідношення різних рівнів тестів у проєкті. Вона зазвичай має форму трикутника і поділяється на три основні шари: основу (юніт-тести), середню частину (інтеграційні тести) та верхівку (сквозні або E2E/UI-тести). Головна ідея моделі полягає в тому, що більшість перевірок має виконуватися на рівні окремих компонентів коду, оскільки вони швидкі, дешеві у написанні та стабільні, тоді як кількість повільних, дорогих і вразливих інтерфейсних тестів має бути зведена до мінімуму.

What is it — The Test Pyramid is a classic conceptual model in QA and software development that defines the ideal ratio of different test levels in a project. It typically takes the shape of a triangle and is divided into three main layers: the base (unit tests), the middle (integration tests), and the peak (end-to-end or E2E/UI tests). The core idea of the model is that the majority of checks should be performed at the individual component code level because they are fast, cheap to write, and stable, while the number of slow, expensive, and fragile UI tests should be kept to a minimum.

Навіщо/коли — Механізм роботи піраміди ґрунтується на економіці розробки та швидкості зворотного зв'язку (feedback loop). Юніт-тести ізольовано й за мілісекунди перевіряють окремі функції чи класи, дозволяючи розробникам запускати їх локально після кожного рядка коду. Інтеграційні тести перевіряють взаємодію між модулями — наприклад, чи правильно сервіс спілкується з базою даних або стороннім API. У той же час, UI-тести імутують реальні дії користувача у браузері, але вони виконуються хвилинами, потребують складного налаштування середовища та часто ламаються через найдрібніші зміни в дизайн-макеті (так звані flaky tests).

Why/when — The mechanism of the pyramid is based on development economics and feedback loop speed. Unit tests check individual functions or classes in isolation in milliseconds, allowing developers to run them locally after every line of code. Integration tests verify interaction between modules—for example, whether a service communicates correctly with a database or a third-party API. At the same time, UI tests simulate real user actions in a browser, but they take minutes to run, require complex environment setup, and frequently break due to minor changes in design layouts (so-called flaky tests).

Приклад з проєкту — У реальному проєкті (наприклад, у розробці фінтех-додатка) ми використовували піраміду як стратегічний орієнтир для автоматизації. Вся бізнес-логіка розрахунку відсотків за кредитом була покрита надійними юніт-тестами (близько 70% від загальної маси тестів). Інтеграційними тестами ми перевіряли коректність запитів до платіжного шлюзу (близько 20%). А на вершині піраміди ми залишили лише 5-7 ключових E2E-сценаріїв — наприклад, повний цикл авторизації, оформлення та успішної оплати кредиту. Такий підхід гарантував нам швидкий запуск CI/CD пайплайну та високу впевненість у релізі.

Project example — In a real project (such as developing a fintech application), we used the pyramid as a strategic guideline for automation. All the business logic for calculating loan interest was covered by robust unit tests (about 70% of the total test mass). With integration tests, we verified the correctness of requests to the payment gateway (about 20%). And at the peak of the pyramid, we left only 5-7 key E2E scenarios—for example, the complete cycle of authorization, application, and successful loan payment. This approach guaranteed a fast CI/CD pipeline run and high confidence in our releases.

Підводні камені — Найпоширенішою помилкою на практиці є побудова «перевернутої піраміди» або «морозильного ріжка», коли команда не пише юніт-тести взагалі, натомість намагаючись покрити всі фічі через важкі UI-тести на Selenium чи Cypress. Це призводить до катастрофічно повільних білдів, які тривають годинами, величезних витрат часу на підтримку тестів і постійних хибно-негативних результатів. Важливо розуміти, що піраміда не є жорсткою догмою, але її фундаментальний економічний принцип — «дешеві тести знизу, дорогі зверху» — залишається незмінним для успішного проєкту.

Pitfalls — The most common mistake in practice is building an "inverted pyramid" or an "ice cream cone," where a team writes no unit tests at all, instead trying to cover every feature through heavy UI tests in Selenium or Cypress. This leads to catastrophically slow builds that take hours, enormous amounts of time spent on test maintenance, and constant false-negative results. It is important to understand that the pyramid is not a rigid dogma, but its fundamental economic principle—"cheap tests at the bottom, expensive at the top"—remains unchanged for a successful project.

Уточнення на співбесіді — Що робити, якщо в проєкті немає unit-тестів, а є лише UI? Почати з API-шару для критичних сценаріїв і домовитися з розробниками, що новий код приходить з unit-тестами.

Interview follow-up — What if the project has no unit tests, only UI ones? Start with an API layer for the critical flows and agree with developers that new code comes with unit tests.

Структура та розподіл рівнів у піраміді тестування
Structure and distribution of levels in the Test Pyramid

+-------------------------------------------------------+
|                 [ E2E / UI Tests ]                    | <- Верхівка: 5-10% обсягу
|  - Інструменти: Cypress, Playwright, Selenium         |    Повільні, дорогі, охоплюють весь додаток
+-------------------------------------------------------+
|             [ Integration Tests ]                     | <- Середина: 20-30% обсягу
|  - Інструменти: Supertest, JUnit / PyTest (з БД)      |    Перевіряють взаємодію між сервісами/API
+-------------------------------------------------------+
|                   [ Unit Tests ]                      | <- Основа: 60-75% обсягу
|  - Інструменти: Jest, PyTest, JUnit, Moq              |    Швидкі, ізольовані, тестують бізнес-логіку
+-------------------------------------------------------+

+-------------------------------------------------------+
|                 [ E2E / UI Tests ]                    | <- Top: 5-10% of the suite
|  - Tools:       Cypress, Playwright, Selenium         |    Slow, expensive, cover the whole app
+-------------------------------------------------------+
|             [ Integration Tests ]                     | <- Middle: 20-30% of the suite
|  - Tools:       Supertest, JUnit / PyTest (+ DB)      |    Check how services/APIs work together
+-------------------------------------------------------+
|                   [ Unit Tests ]                      | <- Base: 60-75% of the suite
|  - Tools:       Jest, PyTest, JUnit, Moq              |    Fast, isolated, test business logic
+-------------------------------------------------------+
💡 Простими словами: Піраміда тестування показує, що більшість перевірок у проєкті мають бути маленькими, швидкими та дешевими (юніт-тести), а повільних і дорогих перевірок через інтерфейс (UI/E2E) має бути зовсім небагато. Це допомагає команді швидко знаходити помилки на ранніх етапах і не витрачати тижні на підтримку нестабільних тестів.
💡 In plain words: The Test Pyramid shows that the majority of checks in a project should be small, fast, and cheap (unit tests), while slow and expensive interface checks (UI/E2E) should be very few. This helps the team find bugs early and avoid wasting weeks maintaining flaky tests.
🗣 Як читати вголос Піраміда тестування — це фундаментальна концепція, яка допомагає правильно побудувати стратегію автоматизації. Її суть у тому, що ми вибудовуємо перевірки у вигляді трикутника: внизу знаходяться юніт-тести, яких має бути найбільше, бо вони виконуються за мілісекунди і є найдешевшими. Посередині розташовані інтеграційні тести для перевірки взаємодії між компонентами чи роботою з базою даних. А на самій верхівці — E2E та UI тести, яких робиться мінімум, оскільки вони повільні й крихкі до змін інтерфейсу. Якщо порушити цей баланс і побудувати «перевернуту піраміду», команда просто потоне в довгих білдах і постійних падіннях тестів через зміну однієї кнопки. Тому на практиці я завжди стежу за тим, щоб основна бізнес-логіка була покрита юнітами, а інтерфейс тестувався лише для критичних шляхів користувача.
🗣 How to say it out loud The Test Pyramid is a foundational concept that helps properly build an automation strategy. The essence is that we structure tests like a triangle: at the bottom are unit tests, which should make up the majority because they run in milliseconds and are the cheapest. In the middle are integration tests to check communication between components or database interactions. At the very top are E2E and UI tests, which are kept to a minimum because they are slow and fragile to interface changes. If you break this balance and build an "inverted pyramid," the team will simply drown in long builds and constant test failures due to changing a single button. Therefore, in practice, I always ensure that core business logic is covered by units, and the UI is tested only for critical user paths.
2 Що таке "крижана вафля" (Ice Cream Cone) антипатерн? What is the Ice Cream Cone anti-pattern?
Концепції тестування та стратегія Testing concepts & strategy senior ▶

Що це — Антипатерн «Крижана вафля» (Ice Cream Cone) — це інверсія класичної піраміди тестування, де більшість зусиль та тестів зосереджено на кінцевому ручному тестуванні та повільних сквозних (E2E) UI-тестах, тоді як базових модульних (Unit) тестів майже немає. У такій системі перевернута структура нагадує перевернуте морозиво на вафельному ріжку, де важка, нестабільна верхівка тримається на надзвичайно слабкому і вразливому фундаменті.

What is it — The "Ice Cream Cone" anti-pattern is an inversion of the classical test pyramid where the majority of testing effort and tests are concentrated on manual testing and slow, brittle end-to-end (E2E) UI tests, while foundational unit tests are virtually non-existent. In such a system, the inverted structure resembles an ice cream cone resting on a fragile base, where a heavy, unstable top-heavy structure balances on weak foundations.

Навіщо/коли — Головна причина виникнення цього антипатерну — хибна впевненість команди в тому, що тестувати бізнес-логіку через користувацький інтерфейс надійніше, або ж відсутність інженерної культури написання юніт-тестів серед розробників. Механізм руйнування полягає в тому, що чим більше тестів виконується на верхньому рівні, тим довше триває зворотний зв'язок (feedback loop), тим дорожче обходиться виправлення дефектів і тим вищий рівень крихкості (flakiness) всієї системи через залежність від зовнішніх факторів, мережі та стану середовища.

Why/when — The primary root cause of this anti-pattern is a team's false confidence that testing business logic through the user interface is more reliable, or a lack of an engineering culture for writing unit tests among developers. The destructive mechanism lies in the fact that the more tests are executed at the upper level, the longer the feedback loop, the more expensive bug fixes become, and the higher the level of system flakiness due to dependencies on external factors, networks, and environment states.

Приклад з проєкту — Типовий сценарій на проєкті з цим антипатерном виглядає так: команда розробників майже не пише модульні тести, повністю покладаючись на QA-інженерів, які намагаються покрити всі сценарії за допомогою важких Selenium- чи Cypress-скриптів або довгих мануальних чекап-листів. Перед кожним релізом запускається величезний сьют E2E-тестів, який постійно падає через непов'язані з бізнес-логікою проблеми (таймаути, мінливу верстку, збої стабілізації оточення), що змушує команду витрачати дні на аналіз «хибно-негативних» (false positive) результатів замість випуску фіч.

Project example — A typical scenario in a project with this anti-pattern looks like this: the development team writes almost no unit tests, relying entirely on QA engineers who try to cover all scenarios using heavy Selenium or Cypress scripts, or long manual check-lists. Before every release, a massive E2E test suite runs, constantly failing due to issues unrelated to business logic (timeouts, layout changes, environment instability), forcing the team to spend days analyzing "false positive" results instead of delivering features.

Підводні камені — Наслідками «крижаної вафлі» стають критично високий час виходу на ринок (Time-to-Market), вигорання команди через рутинне гасіння пожеж перед релізами та величезна вартість підтримки тестової бази. Стратегія виправлення вимагає поступового зміщення акцентів «ліворуч» (Shift-Left): залучення розробників до написання юніт- та інтеграційних тестів, перенесення перевірки бізнес-логіки на рівень сервісів та API, і залишення на рівні E2E лише критичних користувацьких шляхів (Critical Path / Smoke tests).

Pitfalls — The consequences of the "Ice Cream Cone" include critically high Time-to-Market, team burnout due to routine fire-fighting before releases, and immense test maintenance costs. The remediation strategy requires a gradual shift of focus to the left (Shift-Left): engaging developers in writing unit and integration tests, moving business logic verification to the service and API layers, and leaving only critical user journeys (Critical Path / Smoke tests) at the E2E level.

Уточнення на співбесіді — Як переконати команду перевернути конус назад у піраміду? Показати час прогону і частку flaky-падінь UI-набору та переносити перевірки на API-рівень по одній фічі.

Interview follow-up — How do you convince a team to turn the cone back into a pyramid? Show the UI suite's run time and flaky failure rate, and move checks to the API level one feature at a time.

Звіт аудиту тестового покриття (Антипатерн "Крижана вафля")
Test Coverage Audit Report ("Ice Cream Cone" Anti-pattern)
[TEST STRATEGY AUDIT REPORT - PROJECT X]
Timestamp: 2026-03-30 | Status: CRITICAL WARNING

TEST DISTRIBUTION BREAKDOWN:
--------------------------------------------------
[UI / E2E Tests]       ████████████████████  (65%)  - 1,200 tests | Avg execution: 4.5h | Flakiness: 22%
[Integration Tests]    ████                  (15%)  -   300 tests | Avg execution: 25m  | Flakiness: 5%
[Unit Tests]           ██                    (20%)  -   400 tests | Avg execution: 2m   | Flakiness: <1%
--------------------------------------------------

DIAGNOSTIC METRICS:
- Feedback Loop Time: 5+ hours (Blocks CI/CD pipeline)
- Maintenance Cost: 40 hours/week spent fixing broken UI selectors
- Bug Leakage to Prod: High (Edge cases missed due to lack of unit testing)
- Verdict: ICE CREAM CONE ANTI-PATTERN DETECTED. Immediate Shift-Left required.
💡 Простими словами: «Крижана вафля» — це ситуація, коли піраміда тестування перевертається: замість міцного фундаменту з дешевих і швидких юніт-тестів, проєкт спирається на величезну масу повільних та крихких E2E-тестів й ручних перевірок. Це призводить до довгих релізів, частих помилок середовища і колосальних витрат часу на підтримку автоматизації.
💡 In plain words: The "Ice Cream Cone" is when the test pyramid is inverted: instead of a solid foundation of cheap and fast unit tests, the project relies on a massive weight of slow, brittle E2E tests and manual checks. This leads to long releases, frequent environment errors, and massive time spent maintaining test suites.
🗣 Як читати вголос Я добре знайомий із цим антипатерном, оскільки стикався з ним на проєктах, де розробка покладалася переважно на ручне тестування та важкі UI-автотести. Суть «крижаної вафлі» в тому, що піраміда тестів перевертається: базових юніт-тестів майже немає, зате є тисячі повільних E2E-тестів і безкінечні мануальні чеклисти. Через це будь-яка зміна у верстці чи стані бази ламає перевірки, зворотний зв'язок триває годинами, а релізи стають лотереєю. Щоб виправити ситуацію, я зазвичай пропоную стратегію Shift-Left: переносити перевірки на нижчі рівні, залучати розробників до написання юнітів та залишати на рівні UI лише найважливіші бізнес-сценарії.
🗣 How to say it out loud I am very familiar with this anti-pattern, as I've encountered it on projects where development relied heavily on manual testing and heavy UI automated tests. The essence of the "Ice Cream Cone" is that the test pyramid gets inverted: there are virtually no foundational unit tests, but thousands of slow E2E tests and endless manual checklists instead. Because of this, any minor layout or database state change breaks the tests, the feedback loop takes hours, and releases turn into a lottery. To fix this, I usually advocate for a Shift-Left strategy: moving checks down to lower levels, engaging developers in writing unit tests, and keeping only the most essential business flows at the UI level.
3 Що таке Smoke тестування? What is smoke testing?
Концепції тестування та стратегія Testing concepts & strategy junior ▶

Що це — Smoke тестування (або «димове тестування») — це поверхнева перевірка найважливішого функціоналу програми, яка виконується одразу після отримання нової збірки (build) від розробників. Його головна мета — переконатися, що білд у принципі працездатний і критичні компоненти (авторизація, головна сторінка, базові операції) не «зламалися» настільки, що тестувати далі немає сенсу.

What is it — Smoke testing is a preliminary, shallow check of the core functionality of an application, executed immediately after receiving a new build from developers. Its main goal is to ensure the build is basically stable and that critical components (authorization, main page, core operations) are not broken to the point where further testing is a waste of time.

Навіщо/коли — Механізм цього тесту базується на використанні невеликого, але критично важливого набору тест-кейсів (smoke suite), які охоплюють ключові шляхи користувача (Critical Path). Якщо хоча б один критичний тест (наприклад, неможливість зайти в акаунт) провалюється, збірка одразу відхиляється (rejected) і повертається розробникам без проведення повного регресійного чи функціонального тестування, що економить години робочого часу QA-команди.

Why/when — The mechanism relies on a small yet crucial set of test cases (the smoke suite) covering the key user journeys (Critical Path). If even one critical test fails (e.g., inability to log into an account), the build is immediately rejected and sent back to developers without running full regression or functional testing, saving hours of QA working time.

Приклад з проєкту — Smoke тестування обов'язково застосовується на етапі постачання (delivery) нової версії на середовища QA, Staging або Production (після hotfix). Проте його не варто використовувати для пошуку глибоких багів, перевірки валідації полів, зручності інтерфейсу чи граничних значень — для цього існують інші види тестування, такі як функціональний чи граничний аналіз.

Project example — Smoke testing must be applied whenever a new version is delivered to QA, Staging, or Production environments (after a hotfix). However, it should not be used to hunt for deep bugs, verify field validations, check UI polish, or test edge cases—other types of testing exist for those purposes, such as functional or boundary analysis.

Підводні камені — Уявіть реальний проєкт — інтернет-магазин одягу. Розробники викатили на тестове середовище черговий реліз із купою оновлень. Перед тим як тестувати фільтри пошуку чи складні комбінації знижок, QA-інженер запускає smoke-набір: відкриває сайт (чи завантажується головна сторінка?), пробує залогінитися (чи працює авторизація?), додає тестовий товар у кошик (чи додається?). Якщо ці три базові кроки пройшли успішно, збірка приймається в роботу; якщо ні — тестування зупиняється на старті.

Pitfalls — Imagine a real project—an e-commerce clothing store. Developers deployed a new release with many updates to the test environment. Before testing search filters or complex discount combinations, the QA engineer runs the smoke suite: opens the site (does the home page load?), tries to log in (does authorization work?), adds a test item to the cart (does it add successfully?). If these three basic steps pass, the build is accepted for testing; if not, testing is halted right at the start.

Уточнююче питання — Найпоширеніші помилки новачків включають перетворення smoke-тестів на повноцінне регресійне тестування (включення туди сотень кейсів замість 10-20 критичних), ігнорування smoke-перевірки через спеку зі словами «розробники сказали, що все працює», а також виконання таких перевірок вручну там, বিরোধі їх давно можна автоматизувати в рамках CI/CD пайплайну.

Follow-up question — Common mistakes made by beginners include turning smoke tests into full regression testing (adding hundreds of cases instead of 10-20 critical ones), skipping the smoke check due to rush because "developers said everything works," and performing these checks manually where they could easily be automated within a CI/CD pipeline.

Приклад чек-листа для Smoke тестування (Інтернет-магазин)
Smoke testing checklist snippet (E-commerce)
+----+----------------------------+------------------------------------------+------------------+
| ID | Module / Feature           | Smoke Test Description                   | Expected Result  |
+----+----------------------------+------------------------------------------+------------------+
| SM-01| Application Health         | Open base URL and verify HTTP status     | Status 200 OK,   |
|    |                            |                                          | Home page loads  |
| SM-02| Authentication             | Login with valid registered user credentials | User successfully|
|    |                            |                                          | logs into dashboard|
| SM-03| Core Search                | Enter "T-shirt" in search bar and press enter | Search results page|
|    |                            |                                          | shows items      |
| SM-04| Checkout Core Path         | Add item to cart and open checkout screen| Checkout page is |
|    |                            |                                          | accessible       |
+----+----------------------------+------------------------------------------+------------------+
💡 Простими словами: Smoke тестування — це як перевірка, чи заводиться двигун машини та чи вмикаються фари, перш ніж виїжджати на швидкісну трасу. Якщо машина навіть не заводиться, немає жодного сенсу перевіряти роботу кондиціонера чи музичної системи.
💡 In plain words: Smoke testing is like checking if a car engine starts and the headlights turn on before driving onto a highway. If the car won't even start, there is no point in checking the air conditioning or the sound system.
🗣 Як читати вголос Smoke тестування — це базовий і дуже швидкий чек програми, який я проводжу щоразу, коли отримую новий білд від розробників. Уявіть ситуацію: команда щойно викатила оновлення на тестове середовище. Замість того щоб одразу занурюватись у глибоке тестування та шукати дрібні дефекти, я пробігаюся по найважливіших сценаріях — чи відкривається взагалі головна сторінка, чи працює логін і чи можна виконати ключову дію на проєкті. Якщо хоча б один із цих базових компонентів падає, я одразу відхиляю збірку і повертаю її девелоперам, оскільки повноцінне тестування неробочого коду є марною тратою часу.
🗣 How to say it out loud Smoke testing is a basic and very quick check of the application that I run every time I receive a new build from developers. Imagine a situation: the team has just deployed an update to the test environment. Instead of immediately diving into deep testing and hunting for minor defects, I run through the most important scenarios—does the main page even open, does login work, and can a user perform the core action on the project. If even one of these basic components fails, I immediately reject the build and send it back to developers because full-scale testing of broken code is a complete waste of time.
4 Що таке Regression тестування? How to automate regression testing?
Концепції тестування та стратегія Testing concepts & strategy junior ▶

Що це — Регресійне тестування — це перевірка вже існуючого функціоналу програми після внесення будь-яких змін у кодову базу (виправлення багів, додавання нових фіч чи оновлення бібліотек). Його головна мета — переконатися, що нові зміни не зламали те, що раніше працювало коректно.

What is it — Regression testing is the verification of existing application functionality after any changes have been made to the codebase (such as bug fixes, new features, or library updates). Its main goal is to ensure that new changes haven't broken what was previously working correctly.

Навіщо/коли — Механізм регресії полягає у взаємозв’язку компонентів програмного забезпечення. Зміна в одному місці (наприклад, оптимізація запитів до бази даних) може непередбачувано вплинути на суміжні або навіть віддалені модулі через спільні залежності чи глобальні змінні.

Why/when — The mechanism of regression lies in the interconnectedness of software components. A change in one place (e.g., database query optimization) can unpredictably affect adjacent or even distant modules due to shared dependencies or global variables.

Приклад з проєкту — Такий вид тестування застосовують після кожного деплою, виправлення багів або релізу нової ітерації. Повний цикл регресії може бути занадто довгим, тому на практиці використовують підмножину тестів (селективну регресію) або пріоритезовані сценарії на основі критичності бізнес-логіки.

Project example — This type of testing is applied after every deployment, bug fix, or release of a new iteration. A full regression cycle can be too time-consuming, so in practice, a subset of tests (selective regression) or prioritized scenarios based on business logic criticality are used.

Підводні камені — Уявіть проєкт інтернет-магазину, де розробники виправили баг із відображенням знижок у кошику. Під час регресійного тестування QA-інженер перевіряє не лише роботу кошика, а й супутні процеси: чи формується правильний рахунок в особистому кабінеті, чи проходить оплата через платіжний шлюз і чи надходить коректне сповіщення на пошту.

Pitfalls — Imagine an e-commerce project where developers fixed a bug with discount displays in the cart. During regression testing, the QA engineer checks not only the cart functionality but also related processes: whether the correct invoice is generated in the user profile, whether payment goes through the payment gateway, and whether a correct email notification is sent.

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

Follow-up question — The most common mistake for beginners is focusing exclusively on the fixed bug and ignoring adjacent areas, or trying to manually run the entire test suite of the project without automation, which leads to missing defects due to fatigue.

Чек-лист для регресійного тестування (модуль Авторизації)
Regression testing checklist (Authorization module)
[ ] REG-AUTH-01: Login with valid credentials (Happy Path)
[ ] REG-AUTH-02: Login with invalid password (Error handling)
[ ] REG-AUTH-03: Password recovery flow via email
[ ] REG-AUTH-04: Session persistence after page reload
[ ] REG-AUTH-05: Logout functionality & session termination
💡 Простими словами: Це як перевірити, чи заводиться машина і чи працюють фари після того, як автомеханік полагодив у ній коробку передач. Ви перевіряєте суміжні системи, щоб упевнитись, що ремонт не створив нових проблем.
💡 In plain words: It is like checking if the car starts and the headlights work after a mechanic fixes the transmission. You check adjacent systems to ensure the repair didn't create new problems.
🗣 Як читати вголос Регресійне тестування — це перевірка того, чи не зламалося щось старе у проєкті після того, як розробники додали новий код або полагодили якісь баги. Простими словами, ми переконуємося, що наша система продовжує працювати стабільно після будь-яких змін. Зазвичай ми запускаємо такі тести після нових деплоїв або релізів. Я завжди намагаюся визначати пріоритетні сценарії на основі критичності бізнесу, бо перевірити все вручну на великих проєктах фізично неможливо. Головне тут — не просто перевірити виправлену проблему, а проконтролювати усю пов'язану функціональність.
🗣 How to say it out loud Regression testing is checking whether anything old in the project broke after developers added new code or fixed some bugs. In simple terms, we make sure that our system continues to work stably after any changes. Usually, we run these tests after new deployments or releases. I always try to identify priority scenarios based on business criticality, because checking everything manually on large projects is physically impossible. The main thing here is not just to check the fixed issue, but to monitor all related functionality.
5 Що таке Sanity тестування? What is sanity testing?
Концепції тестування та стратегія Testing concepts & strategy junior ▶

Що це — Sanity тестування (санітарне тестування) — це поверхнева і дуже спрямована перевірка, яку проводять після внесення незначних змін до коду (наприклад, виправлення конкретного багу чи мікропатчу), щоб переконатися, що змінена функціональність працює коректно і на перший погляд нічого не зламалося.

What is it — Sanity testing is a very focused and superficial verification conducted after minor code changes—such as a specific bug fix or micro-patch—to ensure that the modified functionality works correctly and that surface-level logic remains intact.

Навіщо/коли — Механіка цього процесу полягає у швидкому, нескриптованому виконанні невеликого набору тестів саме навколо зміненого модуля, без занурення у глибокі залежності системи. Це працює як швидкий "контрольний огляд", який економить час QA-інженера: якщо баг не виправлено або виліз очевидний супутній дефект, білд відхиляється одразу, ще до запуску повноцінного регресійного тестування.

Why/when — The mechanism relies on a quick, unscripted execution of a small subset of tests strictly around the modified module, without diving deep into system dependencies. This acts as a rapid "health check" that saves QA time: if the bug isn't fixed or an obvious regression surfaces immediately, the build is rejected right away before running a full regression suite.

Приклад з проєкту — Цей підхід ідеально застосовувати у ситуаціях, коли розробники випускають терміновий гарячий фікс (hotfix) і потрібно швидко перевірити критичну точку. Проте Sanity тестування абсолютно непридатне для великих релізів чи нових фіч, де потрібне повне покриття вимог, оскільки воно пропускає більшість архітектурних та інтеграційних перевірок.

Project example — This approach is ideal when developers release an urgent hotfix and you need to quickly verify a critical fix point. However, sanity testing is entirely unsuitable for major releases or brand-new features requiring full requirement coverage, as it skips most architectural and integration checks.

Підводні камені — Уявіть проєкт електронної комерції, де виник критичний дефект: застосування купона на знижку іноді призводило до зависання кошика. Розробник випускає виправлення лише для цієї проблеми. Замість того щоб проходити увесь великий сценарій оформлення замовлення, я виконую Sanity тестування: вводжу купон, перевіряю оновлення суми в реальному часі та переконуюся, що кнопка оплати активна — це займає хвилину і дає зелене світло для подальших кроків.

Pitfalls — Imagine an e-commerce project with a critical defect where applying a discount coupon occasionally froze the cart. The developer releases a fix exclusively for this issue. Instead of running through the entire checkout flow, I perform a sanity test: apply the coupon, verify real-time price updates, and ensure the pay button is active—taking just a minute to green-light further testing.

Уточнююче питання — Найпоширеніша помилка новачків — плутати Sanity тестування зі Smoke тестуванням або вважати його повноцінною заміною регресії. Smoke перевіряє працездатність усього додатку загалом після нового збирання, тоді як Sanity зосереджується на вузькій ділянці після зміни; крім того, успішний Sanity-тест не гарантує відсутності прихованих багів у суміжних модулях.

Follow-up question — A common beginner mistake is confusing sanity testing with smoke testing or treating it as a full substitute for regression. Smoke verifies overall application health after a new build, whereas sanity targets a specific module post-fix; moreover, a passing sanity test does not guarantee the absence of hidden bugs in adjacent modules.

Приклад чеклисту для Sanity-тестування виправлення кошика
Example checklist for shopping cart fix sanity testing
[Sanity Check: Cart Discount Bugfix - Build #v1.4.2-hotfix]
- [ ] 1. Apply valid promo code "SAVE20" -> Verify 20% discount applied instantly.
- [ ] 2. Apply expired promo code "OLD10" -> Verify error message appears, cart doesn't crash.
- [ ] 3. Remove promo code -> Verify total price reverts to original correctly.
- [ ] 4. Click "Proceed to Checkout" button -> Verify redirection to payment page works.
💡 Простими словами: Sanity тестування — це швидка точкова перевірка одного конкретного компонента після виправлення багу, щоб впевнитись: те, що ламалося, тепер працює, і не треба гаяти час на перевірку всього додатку.
💡 In plain words: Sanity testing is a quick, targeted check of a single specific component after a bug fix to ensure that what was broken now works, without wasting time testing the entire application.
🗣 Як читати вголос Санітарне тестування — це швидка і цілеспрямована перевірка, яку я проводжу одразу після того, як розробники виправляють конкретний дефект або випускають мікропатч. Головна ідея в тому, щоб не проганяти весь масив регресійних тестів, а переконатися саме у працездатності зміненого шматка коду та його найближчого оточення. Наприклад, якщо пофіксили розрахунок знижки в кошику, я не перевіряю авторизацію чи каталог, а одразу йду в кошик, вводжу промокод і дивлюся на суму. Якщо все ок — передаю білд далі, а якщо ні — одразу відхиляю, заощаджуючи купу часу команді.
🗣 How to say it out loud Sanity testing is a quick, targeted check that I perform right after developers fix a specific defect or drop a micro-patch. The main idea is not to run the entire regression suite, but to ensure that the modified piece of code and its immediate surroundings work correctly. For example, if they fixed the discount calculation in the cart, I don't test authorization or the catalog; I go straight to the cart, enter a promo code, and look at the total. If it looks good, I pass the build forward; if not, I reject it immediately, saving the team a lot of time.
6 Чи можна автоматизувати Exploratory тестування? Can exploratory testing be automated?
Концепції тестування та стратегія Testing concepts & strategy junior ▶

Що це — Експлораторне (дослідницьке) тестування — це підхід до забезпечення якості, де етапи проектування тестів та їх виконання відбуваються одночасно. Тестувальник не використовує передстворені детальні сценарії з покроковими інструкціями, а досліджує систему динамічно, спираючись на власний досвід, інтуїцію, знання домену та результати попередніх кроків.

What is it — Exploratory testing is a QA approach where test design and test execution are performed simultaneously. Instead of following pre-scripted, detailed test cases with step-by-step instructions, the tester dynamically investigates the system based on personal experience, intuition, domain knowledge, and feedback from previous actions.

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

Why/when — The mechanism of this approach relies on a continuous learning loop: "observe, hypothesize, experiment, and analyze." The tester builds mental models of how a feature works, quickly forms assumptions about where defects might hide (e.g., in field validation, async requests, or edge values), and immediately tests them live, adapting subsequent actions based on the outcomes.

Приклад з проєкту — Цей метод є надзвичайно ефективним на ранніх етапах розробки, коли специфікації відсутні або постійно змінюються, при жорстких дедлайнах для регресійного тестування, а також для виявлення неочікуваних UI/UX та логічних багів. Проте він не замінює формалізовані скриптовані тести і не підходить для суворих регульованих аудитів, де вимагається 100% трасованість вимог та документальне підтвердження кожного кроку.

Project example — This method is extremely effective during early development stages when specifications are missing or constantly changing, under tight deadlines for regression testing, and for uncovering unexpected UI/UX and logical bugs. However, it does not replace formal scripted tests and is not suitable for strict regulated audits that require 100% requirement traceability and documented proof of every single step.

Підводні камені — У реальному проєкті ми часто використовуємо сесійне тестування (Session-Based Testing) із прив'язкою до чартерів. Наприклад, перед релізом оновлення кошика інтернет-магазину я проводжу 45-хвилинну сесію з фокусом на сценарії швидкого чекауту при нестабільному з'єднанні, фіксуючи нові знахідки, ризики та потенційні вузькі місця «на льоту» в спеціальному звіті.

Pitfalls — In a real project, we frequently use session-based testing bound to specific charters. For example, before releasing an e-commerce cart update, I run a 45-minute session focused on quick checkout scenarios under unstable network connectivity, logging new findings, risks, and potential bottlenecks on the fly in a session sheet.

Уточнююче питання — Типова помилка початківців — сприймати експлораторне тестування як хаотичне «тицяння по екрану» без мети та аналізу. Насправді це дисциплінована пізнавальна діяльність, яка вимагає глибокого розуміння бізнес-логіки, ведення нотаток та постійного критичного мислення.

Follow-up question — A common mistake among juniors is treating exploratory testing as chaotic, random clicking on the screen with no goal or analysis. In reality, it is a disciplined cognitive activity that requires a deep understanding of business logic, structured note-taking, and continuous critical thinking.

Фрагмент плану сесії дослідницького тестування (Session Charter)
Exploratory Testing Session Charter Snippet
Session ID: EXP-CART-01
Duration: 45m
Charter: Explore cart calculations and checkout edge cases under simulated network throttling.
Areas to cover: Promo code application, cart item removal, offline mode recovery.
Bugs found:
- BUG-104: Cart total fails to update when promo code is applied via Enter key.
- BUG-105: Infinite spinner on checkout button if network drops during payload submission.
Notes: Checked integration with payment gateway sandbox; looks stable under 3G throttling.
💡 Простими словами: Експлораторне тестування — це як дослідження незнайомого міста без карти, але з чіткою метою: вивчати додаток, думати як справжній користувач і шукати слабкі місця прямо під час взаємодії з ним.
💡 In plain words: Exploratory testing is like exploring an unfamiliar city without a map but with a clear purpose: learning the application, thinking like a real user, and hunting for flaws on the fly while interacting with it.
🗣 Як читати вголос Експлораторне тестування — це підхід, де я не пишу детальні тест-кейси заздалегідь, а поєдную дизайн тестів та їх виконання в один процес. Я використовую його тоді, коли документації мало або обмежений час, бо це дозволяє швидко знайти несподівані баги завдяки моєму досвіду та інтуїції. Наприклад, ми часто проводимо сесійне тестування за чітким чартером перед релізом критичних фіч. Це зовсім не хаотичне тикання по екрану, а структурований процес навчання й аналізу поведінки програми. Головне тут — добре розуміти бізнес-логіку і постійно ставити під сумнів те, як система реагує на нестандартні дії.
🗣 How to say it out loud Exploratory testing is an approach where I don't write detailed test cases in advance, but instead combine test design and execution into a single process. I use it when documentation is scarce or time is tight, because it allows me to quickly uncover unexpected bugs using my experience and intuition. For example, we frequently run time-boxed exploratory sessions based on specific charters before releasing critical features. It's definitely not random clicking around the screen; rather, it's a structured process of learning and analyzing app behavior. The key here is having a solid grasp of the business logic and constantly questioning how the system responds to non-standard actions.
7 Що таке End-to-End (E2E) тестування? What is End-to-End (E2E) testing?
Концепції тестування та стратегія Testing concepts & strategy junior ▶

Що це — End-to-End (E2E) тестування — це метод перевірки цілісності та працездатності всієї програмної системи від початку й до кінця. Воно імітує реальні дії користувача в умовах, максимально наближених до бойових (Production), перевіряючи взаємодію між усіма компонентами: фронтендом, бекендом, базами даних, сторонніми API та платіжними шлюзами.

What is it — End-to-End (E2E) testing is a methodology used to test whether the flow of an application is behaving as designed from start to finish. It simulates real user behavior in an environment closely resembling production, validating the integration and communication between all components like frontend, backend, databases, third-party APIs, and payment gateways.

Навіщо/коли — Механізм E2E полягає в тому, що тест запускається через користувацький інтерфейс (браузер чи мобільний додаток) і простежує весь життєвий цикл даних. Система запитує інформацію з бази даних, обробляє бізнес-логіку на сервері, надсилає зовнішній запит до сторонніх сервісів і повертає оновлений UI. На відміну від юніт-тестів чи інтеграційних перевірок, E2E розглядає систему як «чорну скриньку», фокусуючись виключно на сквозних бізнес-сценаріях.

Why/when — The mechanism of E2E relies on running tests through the user interface (a browser or mobile app) and tracking the entire data lifecycle. The system queries the database, processes business logic on the server, triggers external requests to third-party services, and updates the UI. Unlike unit or integration tests, E2E treats the system as a "black box," focusing purely on end-to-end business workflows.

Приклад з проєкту — Стратегічно E2E-тести варто застосовувати для найкритичніших бізнес-процесів (checkout, реєстрація, авторизація, транзакції), де збій безпосередньо шкодить фінансам компанії. Водночас їх не слід використовувати для покриття кожної дрібної валідації форми чи низькорівневої логіки, оскільки E2E-тести повільні у виконанні, чутливі до змін навколишнього середовища та складні у підтримці (знаходяться на верхівці Тестової Піраміди).

Project example — Strategically, E2E tests should be applied to the most critical business paths (such as checkout, registration, authorization, and transactions) where failures directly impact company revenue. However, they should not be used to cover every minor UI validation or low-level logic, as E2E tests are slow to execute, sensitive to environment changes, and expensive to maintain, sitting at the very top of the Test Pyramid.

Підводні камені — Уявімо типовий проєкт електронної комерції. Реальний E2E-сценарій починається з того, що автоматизований скрипт відкриває сайт, виконує логін користувача, додає товар до кошика, переходить на сторінку чекауту, симулює введення реквізитів картки, підтверджує транзакцію через платіжний шлюз і врешті перевіряє, чи змінився статус замовлення в базі даних та чи надійшов лист підтвердження.

Pitfalls — Imagine a typical e-commerce project. A real-world E2E scenario starts with an automated script opening the website, logging in a user, adding a product to the cart, proceeding to checkout, simulating credit card details entry, confirming the transaction via a payment gateway, and finally verifying that the order status updated in the database and a confirmation email was dispatched.

Уточнююче питання — Найпоширеніша помилка початківців — намагатися покрити 100% функціональності за допомогою E2E-тестів (створення антипатерну «морозиво» замість піраміди), а також жорстке зашивання тестових даних і використання нестабільних локаторів верстки. Здоровий підхід передбачає написання стабільних тестів із використанням унікальних атрибутів (data-testid) та динамічної генерації даних для кожного запуску.

Follow-up question — A common mistake among junior testers is trying to cover 100% of functionality using E2E tests—creating an "ice cream cone" anti-pattern instead of a pyramid—along with hardcoded test data and brittle UI selectors. A sound approach involves writing resilient tests by utilizing dedicated attributes (data-testid) and dynamic data generation for each run.

Приклад E2E тесту (Playwright / TypeScript)
E2E test example (Playwright / TypeScript)
import { test, expect } from '@playwright/test';

test('User can complete checkout successfully', async ({ page }) => {
  // 1. Open app and login
  await page.goto('https://shop.example.com/login');
  await page.fill('[data-testid="email-input"]', 'user@example.com');
  await page.fill('[data-testid="password-input"]', 'SecurePass123!');
  await page.click('[data-testid="login-btn"]');

  // 2. Add product and go to cart
  await page.click('[data-testid="product-item"]:first-child');
  await page.click('[data-testid="add-to-cart-btn"]');
  await page.goto('https://shop.example.com/cart');

  // 3. Checkout process
  await page.click('[data-testid="checkout-btn"]');
  await page.fill('[data-testid="address-input"]', '123 Test St');
  await page.click('[data-testid="place-order-btn"]');

  // 4. Assert success message and DB state via UI
  await expect(page.locator('[data-testid="success-banner"]')).toBeVisible();
  await expect(page.locator('[data-testid="order-number"]')).not.toBeEmpty();
});
💡 Простими словами: End-to-End тестування перевіряє роботу всієї системи «від кліка в браузері до бази даних та сторонніх сервісів», імітуючи шлях реального користувача ключовим бізнес-сценарієм. Це останній рубіж оборони перед релізом у production.
💡 In plain words: End-to-End testing verifies the entire system workflow from browser clicks down to the database and third-party services, simulating a real user’s journey through a critical business scenario. It is the final defense line before a production release.
🗣 Як читати вголос End-to-End тестування — це перевірка всієї програми від початку до кінця в умовах, максимально наближених до реальних. Я розумію це як симуляцію дій користувача через інтерфейс, яка зачіпає фронтенд, бекенд, базу даних та зовнішні інтеграції. На практиці ми використовуємо E2E для найважливіших бізнес-процесів, таких як оформлення замовлення чи реєстрація. При цьому я добре розумію тестову піраміду, тому намагаюся не перевантажувати проєкт сотнями повільних E2E-тестів, залишаючи їх лише для найкритичніших місць. Головне правило тут — використовувати унікальні селектори на кшталт data-testid, щоб тести не ламалися через зміни у верстці.
🗣 How to say it out loud End-to-End testing is checking the entire application from start to finish under conditions closely resembling production. I see it as simulating a user's actions through the interface, touching the frontend, backend, database, and external integrations. In practice, we use E2E for the most crucial business flows like checkout or registration. At the same time, I understand the test pyramid well, so I try not to overload the project with hundreds of slow E2E tests, reserving them strictly for critical paths. The main rule here is to use resilient locators like data-testid so tests don't break with UI updates.
8 Що таке flaky test? What is a flaky test?
Концепції тестування та стратегія Testing concepts & strategy junior ▶

Що це — Flaky test (нестабільний тест) — це такий тест, який може як успішно пройти, так і впасти за однакових умов виконання на тому самому коді, без будь-яких змін у самій програмі чи у файлах тестів. Іншими словами, він демонструє непередбачувану поведінку, видаючи «хибнопозитивні» або «хибнонегативні» результати при повторних запусках.

What is it — A flaky test (unstable test) is a test that can either pass successfully or fail under the exact same execution conditions on the same code, without any changes to the application or test files. In other words, it exhibits unpredictable behavior, producing false positives or false negatives upon re-runs.

Навіщо/коли — Головна причина появи flaky тестів криється у зовнішніх факторах та проблемах синхронізації. Це можуть бути мережеві затримки (network latency), асинхронні запити, стани гонки (race conditions), спільні ресурси бази даних або жорстко зашиті таймаути (hardcoded timeouts), які не встигають спрацювати при повільнішому завантаженні середовища виконання на CI-сервері.

Why/when — The main root cause of flaky tests lies in external factors and synchronization issues. These can include network latency, asynchronous requests, race conditions, shared database resources, or hardcoded timeouts that fail to trigger when the execution environment is running slower on the CI server.

Приклад з проєкту — На реальному проєкті з автоматизації UI-тестування (наприклад, за допомогою Cypress чи Playwright) я часто стикався з ситуацією, коли тест на перевірку появи модального вікна падав на CI приблизно в 20% випадків. Причиною було те, що бекенд-відповідь на мікросекунду затримувалася довше, ніж тривав фіксований час очікування (cy.wait(500)), хоча локально на потужному ноутбуці все працювало ідеально.

Project example — In a real UI automation project (e.g., using Cypress or Playwright), I frequently encountered a situation where a test verifying a modal window opening would fail on CI in about 20% of runs. The reason was that the backend response took a microsecond longer than the fixed wait time (cy.wait(500)), even though everything worked perfectly locally on a powerful laptop.

Підводні камені — Нестабільні тести є критичною проблемою для команди, оскільки вони підривають довіру до всієї системи автоматизації та результатів збірки (CI/CD). Поширена помилка новачків — просто масово вмикати вбудовані механізми повтору (retries) замість того, щоб шукати та усувати першопричину: переходити на явні очікування (explicit waits), стабілізувати тестові дані або ізолювати середовище виконання.

Pitfalls — Flaky tests are a critical problem for a team because they undermine trust in the entire automation suite and CI/CD build results. A common mistake made by newcomers is to simply mass-enable built-in retry mechanisms instead of investigating and fixing the root cause: switching to explicit waits, stabilizing test data, or isolating the execution environment.

Уточнення на співбесіді — Як довести, що тест саме flaky, а не ловить реальний баг? Прогнати його 20–50 разів на тому ж коміті: якщо результат змінюється без змін коду, це нестабільність.

Interview follow-up — How do you prove a test is flaky rather than catching a real bug? Run it 20–50 times on the same commit: if the result changes without code changes, it is instability.

Фрагмент ненадійного тест-кейсу (Flaky test) та його виправлення
Snippet of an unreliable test case (Flaky test) and its fix
// ❌ Flaky test: залежить від жорсткого таймауту та швидкості мережі
it('should load user profile', () => {
  cy.get('#load-profile').click();
  cy.wait(1000); // Погано: на CI-сервері запит може тривати довше
  cy.get('.profile-name').should('be.visible');
});

// ✅ Стабільний тест: використовує явне очікування (explicit wait)
it('should load user profile', () => {
  cy.get('#load-profile').click();
  cy.get('.profile-name', { timeout: 10000 }).should('be.visible');
});
// ❌ Flaky test: depends on a hard timeout and network speed
it('should load user profile', () => {
  cy.get('#load-profile').click();
  cy.wait(1000); // Bad: on the CI server the request may take longer
  cy.get('.profile-name').should('be.visible');
});

// ✅ Stable test: uses an explicit wait
it('should load user profile', () => {
  cy.get('#load-profile').click();
  cy.get('.profile-name', { timeout: 10000 }).should('be.visible');
});
💡 Простими словами: Флейкі-тест — це «капризний» тест, який видає різні результати при однакових вхідних даних: то проходить, то падає. Головна небезпека полягає в тому, що команда звикає ігнорувати такі падіння, через що можна пропустити реальний критичний дефект у продакшн-коді.
💡 In plain words: A flaky test is a "temperamental" test that produces different results with the same input data—sometimes passing, sometimes failing. The main danger is that the team gets used to ignoring such failures, which can lead to missing a real critical bug in production code.
🗣 Як читати вголос Флейкі-тестом ми називаємо тест, який поводиться нестабільно: сьогодні він зелений, а завтра червоний, хоча сам код програми чи тесту ніхто не чіпав. Зазвичай це трапляється через асинхронні процеси, мережеві затримки або жорстко прописані таймаути, коли тест випереджає завантаження елементів. На практиці я стикався з цим в UI-автоматизації, коли елементи на CI-сервері завантажувалися повільніше через нестачу ресурсів. Боротися з цим потрібно не просто увімкненням ретраїв, а виправленням кореневої причини — наприклад, переходом на правильні явні очікування або ізоляцією тестових даних. Якщо ігнорувати флейкі-тести, згодом вся команда просто перестане довіряти звітам автоматизації.
🗣 How to say it out loud By a flaky test, we mean a test that behaves inconsistently: it's green today and red tomorrow, even though no one touched the application or test code. This usually happens due to asynchronous processes, network delays, or hardcoded timeouts when the test races ahead of element loading. In practice, I've encountered this in UI automation when elements on the CI server loaded slower due to resource constraints. We need to combat this not just by turning on retries, but by fixing the root cause—such as switching to proper explicit waits or isolating test data. If you ignore flaky tests, eventually the entire team will simply stop trusting the automation reports.
9 Що таке test data management в автоматизації? What is test data management in automation?
Концепції тестування та стратегія Testing concepts & strategy senior ▶

Що це — Test Data Management (TDM) — це комплекс стратегій, інструментів та процесів для створення, забезпечення, маскування, безпеки та життєвого циклу тестових даних у середовищах розробки й тестування. На рівні Senior-спеціаліста TDM виходить за межі простого завантаження дампа продакшену; це автоматизована система, яка гарантує наявність валідних, унікальних та безпечних даних саме тоді, коли це потрібно пайплайну тестування.

What is it — Test Data Management (TDM) is a comprehensive set of strategies, tools, and processes for creating, provisioning, masking, securing, and managing the lifecycle of test data across development and testing environments. At a senior level, TDM goes far beyond simply restoring a production database dump; it is an automated pipeline that ensures valid, unique, and secure data is available precisely when the testing workflow requires it.

Навіщо/коли — Механізм роботи TDM будується навколо трьох основних компонентів: генерація синтетичних даних, маскування чутливої інформації (PII/GDPR-комплаєнс) та віртуалізація чи клонування баз даних. Автоматизація тут критична: замість ручного створення користувачів через UI, TDM-інструменти (або кастомні скрипти) інжектують необхідні набори даних через API чи безпосередньо в БД перед стартом тестових прогонів, мінімізуючи накладні витрати на підготовку середовища.

Why/when — The TDM mechanism revolves around three core components: synthetic data generation, masking of sensitive information (PII/GDPR compliance), and database virtualization or cloning. Automation is critical here: instead of manual UI data creation, TDM tools (or custom scripts) inject required data sets via APIs or directly into the database before test runs begin, minimizing test environment setup overhead.

Приклад з проєкту — TDM життєво необхідний у масштабних ентерпрайз-системах, фінансовому секторі та е-комерції, де розгортання тестів вимагає мільйонів рядків пов'язаних транзакційних даних, а правила безпеки забороняють використання реальних даних користувачів. Водночас, для маленьких стартапів чи простих мікросервісів з мінімальною кількістю сутностей повномасштабне впровадження важких ентерпрайз-платформ TDM є надлишковим — тут достатньо легких SQL-скриптів ініціалізації та фабрик тестових даних.

Project example — TDM is vital in large-scale enterprise systems, the financial sector, and e-commerce where test execution requires millions of rows of relational transactional data, and security policies strictly forbid using real user data. Conversely, for small startups or simple microservices with minimal entity relationships, implementing heavy enterprise TDM platforms is overkill; lightweight SQL initialization scripts and test data factories are sufficient.

Підводні камені — У реальному проєкті для фінансової платформи ми зіткнулися з проблемою, коли QA-автоматизатори чекали годинами на оновлення тестової БД або падали через конфлікти унікальних ключів під час паралельного запуску тисяч тестів. Впровадження стратегії TDM із динамічним наповненням даних на рівні API та регулярним мікроклонуванням чистих станів БД за допомогою контейнеризації знизило час підготовки середовища на 75% і повністю усунуло флейкі-тести, пов'язані з брудним станом даних.

Pitfalls — In a real project for a financial platform, we faced an issue where QA automation engineers waited hours for test DB refreshes or failed due to Duplicate Key Errors during parallel execution of thousands of tests. Implementing a TDM strategy with dynamic API-level data provisioning and regular micro-cloning of clean DB states using containerization reduced environment setup time by 75% and completely eliminated flaky tests caused by dirty data states.

Уточнююче питання — Поширеною помилкою є копіювання production-баз без належного маскування, що створює серйозні ризики безпеки та порушує регуляторні вимоги на кшталт GDPR, а також відсутність процедур очищення (teardown), що призводить до розростання баз даних «сміттєвими» записами. Інший антипаттерн — покладати всю відповідальність за управління даними виключно на ручне тестування замість інтеграції TDM-процесів безпосередньо у CI/CD-пайплайн.

Follow-up question — A common mistake is copying production databases without proper masking, posing severe security risks and violating regulatory requirements like GDPR, as well as lacking teardown procedures, which leads to database bloat with garbage records. Another anti-pattern is placing sole responsibility for data management exclusively on manual testing instead of integrating TDM processes directly into the CI/CD pipeline.

Конфігурація правил маскування та генерації даних (JSON)
Data Masking and Generation Rules Configuration (JSON)
{
  "dataset": "user_billing_profile",
  "version": "1.4.2",
  "strategies": [
    {
      "column": "email",
      "action": "mask",
      "method": "format_preserving_pseudonymization",
      "domain": "test.qa.enterprise.internal"
    },
    {
      "column": "credit_card_number",
      "action": "synthesize",
      "generator": "luhn_valid_visa",
      "preserve_nulls": false
    },
    {
      "column": "ssn_tax_id",
      "action": "obfuscate",
      "method": "randomize_pattern",
      "pattern": "###-##-####"
    }
  ],
  "provisioning": {
    "target_env": "staging-qa-03",
    "cloning_mode": "snapshot",
    "teardown_on_complete": true
  }
}
💡 Простими словами: Test Data Management — це як розумна система доставки необхідних інгредієнтів на кухню перед готуванням: вона автоматично створює, очищує та захищає дані для тестів, щоб перевірки проходили швидко, безпечно і без збоїв через відсутність потрібної інформації.
💡 In plain words: Test Data Management is like a smart delivery system bringing the right ingredients to the kitchen before cooking: it automatically creates, cleans, and secures test data so that test runs execute quickly, safely, and without failures caused by missing information.
🗣 Як читати вголос Test Data Management для мене — це не просто бекап продакшену, а наскрізна стратегія роботи з даними впродовж усього циклу розробки. На попередніх проєктах ми часто стикалися з тим, що автоматизація падала через застарілі або зайняті дані, і розробники витрачали купу часу на ручний підгон баз. Тому ми побудували систему, яка автоматично генерує синтетичні сутності через API та створює чисті сліпки БД під кожен паралельний запуск тестів. Головне тут — дотримуватися правил безпеки та GDPR через маскування чутливих полів, а також автоматизувати очищення даних, щоб середовище не деградувало з часом.
🗣 How to say it out loud For me, Test Data Management is not just about database backups, but a comprehensive strategy for handling data throughout the entire development cycle. On previous projects, we often faced automation failures due to outdated or locked data, with developers wasting tons of time manually fixing database states. That is why we built a system that automatically generates synthetic entities via APIs and provisions clean database snapshots for every parallel test run. The key here is adhering to security and GDPR compliance through sensitive data masking, as well as automating data teardown so that environments do not degrade over time.
10 Що таке AAA патерн у тестах? What is the AAA pattern in tests?
Концепції тестування та стратегія Testing concepts & strategy junior ▶

Що це — Патерн AAA (Arrange-Act-Assert) — це загальноприйнятий підхід до структурування модульних (unit) та інтеграційних тестів, який розділяє кожну тестову функцію на три чіткі логічні фази. Фаза Arrange відповідає за підготовку всього необхідного середовища (створення об'єктів, налаштування моків та ініціалізацію початкових даних). Фаза Act виконує саму тестовану дію — наприклад, викликає функцію чи метод, який ми перевіряємо. Нарешті, фаза Assert порівнює отриманий результат із нашими очікуваннями.

What is it — The AAA (Arrange-Act-Assert) pattern is a universally accepted approach to structuring unit and integration tests, dividing each test function into three distinct logical phases. The Arrange phase handles setting up the necessary environment (creating objects, configuring mocks, and initializing initial data). The Act phase performs the action under test — for instance, calling the function or method we are checking. Finally, the Assert phase compares the actual result against our expectations.

Навіщо/коли — Цей підхід працює завдяки принципу єдиної відповідальності для тесту, забезпечуючи високу читабельність та передбачуваність. Коли розробник або QA-інженер відкриває тестовий файл, він одразу бачить чітку межу між тим, *що* готується для перевірки, *що саме* виконується та *що саме* верифікується. Це знижує когнітивне навантаження та дозволяє миттєво локалізувати проблему в разі падіння тесту: якщо помилка в секції Assert — проблема у бізнес-логіці результату, а якщо в Arrange — у підготовці тестового оточення.

Why/when — This approach works thanks to the single-responsibility principle for tests, ensuring high readability and predictability. When a developer or QA engineer opens a test file, they instantly see a clear boundary between *what* is being prepared for the check, *what* is being executed, and *what* is being verified. This reduces cognitive load and allows for immediate problem localization if a test fails: if the failure is in the Assert section, the bug is in the result business logic; if in Arrange, it's in the test environment setup.

Приклад з проєкту — На реальному проекті електронної комерції цей патерн застосовується постійно. Наприклад, створюючи тест для перевірки застосування промокоду, у блоці *Arrange* ми ініціалізуємо об'єкт користувача з певним балансом та активний промокод зі знижкою. У блоці *Act* ми викликаємо метод applyPromoCode(user, promo), який додає знижку до кошика. У блоці *Assert* ми перевіряємо, що кінцева сума замовлення зменшилася на правий відсоток, а статус промокоду в базі даних змінився на використаний.

Project example — On a real e-commerce project, this pattern is applied constantly. For example, when creating a test to check promo code application, in the *Arrange* block we initialize a user object with a specific balance and an active discount promo code. In the *Act* block, we call the applyPromoCode(user, promo) method, which applies the discount to the cart. In the *Assert* block, we verify that the final order total decreased by the correct percentage and the promo code status in the database changed to used.

Підводні камені — Найпоширенішими помилками при використанні AAA є розмиття меж між фазами, змішування викликів дій із перевірками або наявність кількох блоків Act в одному тесті. Ще одна проблема — занадто роздутий блок Arrange без використання хелперів чи фабрик тестових даних (Data Factories), через що тести стають крихкими, захаращеними та складними для підтримки при зміні структури даних.

Pitfalls — The most common mistakes when using AAA include blurring the boundaries between phases, mixing action calls with assertions, or having multiple Act blocks in a single test. Another issue is an overly bloated Arrange block without using helper methods or test data factories, which makes tests brittle, cluttered, and difficult to maintain when data structures change.

Уточнення на співбесіді — Чи можна мати кілька assert в секції Assert? Так, якщо вони перевіряють один результат дії; дві різні дії в одному тесті — вже сигнал розділити його.

Interview follow-up — Can the Assert section hold several asserts? Yes, if they check the outcome of one action; two different actions in one test is a signal to split it.

Приклад структури тесту за паттерном AAA (JavaScript / Jest)
Example of test structure using the AAA pattern (JavaScript / Jest)
test('should calculate discount correctly for VIP user', () => {
  // Arrange
  const user = { id: 1, type: 'VIP', balance: 1000 };
  const cart = [{ id: 101, price: 200, quantity: 2 }];
  const discountService = new DiscountService();

  // Act
  const finalPrice = discountService.calculateTotal(user, cart);

  // Assert
  expect(finalPrice).toBe(360); // 400 with 10% VIP discount applied
});
💡 Простими словами: Паттерн AAA — це кулінарна формула для твоїх тестів: спочатку наріж і розклади інгредієнти (Arrange), потім змішай та приготуй страву (Act), а наприкінці спробуй і оціни смак (Assert). Це робить тестовий код зрозумілим для кожного в команді без довгих роздумів.
💡 In plain words: The AAA pattern is a cooking formula for your tests: first chop and lay out your ingredients (Arrange), then mix and cook the dish (Act), and finally taste and evaluate the flavor (Assert). This makes test code easy for anyone on the team to understand without overthinking.
🗣 Як читати вголос Я завжди намагаюся чітко дотримуватися паттерну AAA, оскільки він робить код тестів чистим, передбачуваним і дуже легким для підтримки. Назви фаз — Arrange, Act та Assert — допомагають мені візуально розділити підготовку даних, саму дію та перевірку результату. На практиці це означає, що ніхто в команді не заплутається у тому, де саме готується оточення, а де тестується логіка. Якщо якийсь тест падає, я одразу бачу, у якій із цих трьох зон виникла проблема. Крім того, я завжди залишаю порожні рядки між цими секціями для кращої читабельності.
🗣 How to say it out loud I always try to strictly follow the AAA pattern because it keeps test code clean, predictable, and very easy to maintain. The phase names — Arrange, Act, and Assert — help me visually separate data preparation, the action itself, and the result verification. In practice, this means nobody on the team gets confused about where the environment is set up versus where the logic is tested. If a test fails, I immediately see which of these three zones the problem came from. Plus, I always leave blank lines between these sections for better readability.
▶ 2 18
11 Яка анотація JUnit5 позначає тест-метод? Which JUnit5 annotation marks a test method?
JUnit5 та TestNG фреймворки JUnit5 and TestNG frameworks junior ▶

Що це — В основі фреймворку JUnit 5 (модуль Jupiter) лежить анотація @Test, яка використовується для позначення методу як виконуваного тестового кейсу. На відміну від попередніх версій, де використовувався пакет org.junit.Test, у JUnit 5 ця анотація імпортується з пакета org.junit.jupiter.api. Вона чітко сигналізує тестовому рушію (test runner), що даний метод є незалежною перевіркою певної функціональності або бізнес-правила.

What is it — At the core of the JUnit 5 framework (Jupiter module) lies the @Test annotation, which is used to mark a method as an executable test case. Unlike previous versions that used the org.junit.Test package, in JUnit 5 this annotation is imported from the org.junit.jupiter.api package. It clearly signals to the test runner that the given method is an independent verification of a specific feature or business rule.

Навіщо/коли — Механізм роботи @Test базується на скануванні класів під час виконання (runtime) за допомогою рефлексії. Коли фреймворк запускає тестовий клас, він шукає всі методи, позначені цією анотацією, створює новий екземпляр тестового класу (за замовчуванням за життєвим циклом PER_METHOD) і послідовно виконує їх. Будь-які твердження (assertions) або винятки, перехоплені під час виконання цього методу, безпосередньо впливають на загальний статус звіту про проходження тестів.

Why/when — The mechanism behind @Test is based on runtime scanning using reflection. When the framework executes a test class, it scans for all methods annotated with this annotation, creates a new instance of the test class (by default per method lifecycle), and invokes them sequentially. Any assertions or exceptions caught during the execution of this method directly determine the overall pass/fail status in the test report.

Приклад з проєкту — Цю анотацію варто використовувати для кожного окремого юніт- чи інтеграційного тесту, який перевіряє конкретний стан або перехід. Проте існують обмеження: метод із @Test зазвичай повинен мати типом повернення void та не приймати традиційних параметрів (якщо не використовуються спеціальні розширення параметрів, такі як TestInfo або RepetitionInfo), оскільки фреймворк не знає, які саме значення передавати у звичайні аргументи.

Project example — You should use this annotation for every single unit or integration test that verifies a specific state or transition. However, there are restrictions: a method marked with @Test must typically have a void return type and cannot accept traditional parameters (unless special parameter resolvers like TestInfo or RepetitionInfo are used), as the framework does not know what values to pass into standard arguments.

Підводні камені — У реальному проєкті автоматизації я пишу тести для сервісного шару електронної комерції, де анотація @Test застосовується до методів на кшталт shouldCalculateDiscountForVipUserCorrectly(). Це дозволяє чітко ізолювати бізнес-логіку знижок від решти системи та швидко локалізувати проблему в разі регресії. Кожен такий метод зосереджений лише на одній перевірці, що робить кодову базу тестів підтримуваною та читабельною.

Pitfalls — In a real automation project, I write tests for an e-commerce service layer where the @Test annotation is applied to methods like shouldCalculateDiscountForVipUserCorrectly(). This allows for clear isolation of discount business logic from the rest of the system and quick problem localization during a regression. Each such method focuses on a single check, making the test codebase maintainable and readable.

Поширеною помилкою серед початківців є випадковий імпорт анотації @Test із пакетів JUnit 4 замість JUnit 5, через що тестовий рушій просто ігнорує метод і не запускає його. Інша типова помилка — оголошення тестового методу з модифікатором доступу private; хоча JUnit 5 у деяких конфігураціях може це допускати через рефлексію, найкращою практикою є використання пакункового (package-private) або public доступу.

Follow-up question — A common mistake among beginners is accidentally importing the @Test annotation from JUnit 4 packages instead of JUnit 5, causing the test runner to silently ignore and skip the method. Another typical error is declaring the test method with a private access modifier; while JUnit 5 may allow this via reflection in certain configurations, best practice dictates using package-private or public access.

Уточнення на співбесіді — Чим @Test з JUnit5 відрізняється від @Test у TestNG? У JUnit5 він не має атрибутів на кшталт priority чи groups — для цього є @Order і @Tag.

Interview follow-up — How does JUnit5 @Test differ from TestNG @Test? In JUnit5 it has no attributes like priority or groups — @Order and @Tag cover that.

Приклад використання анотації @Test у JUnit 5
Example of using the @Test annotation in JUnit 5
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class CalculatorTest {

    @Test
    void shouldAddTwoPositiveNumbersCorrectly() {
        Calculator calculator = new Calculator();
        int result = calculator.add(5, 7);
        assertEquals(12, result, "5 + 7 should equal 12");
    }
}
💡 Простими словами: Анотація @Test — це основний маячок для JUnit 5, який говорить фреймворку: «Це мій тест, запусти його під час перевірки». Без цієї анотації середовище розробки та збирач просто проігнорують ваш метод, вважаючи його звичайною допоміжною функцією.
💡 In plain words: The @Test annotation is a core flag for JUnit 5 that tells the framework, "This is my test, run it during execution." Without this annotation, the build tool and test runner will simply ignore your method, treating it as an ordinary utility function.
🗣 Як читати вголосГоловною анотацією для позначення тест-методу в JUnit 5 є @Test, яка імпортується з пакета org.junit.jupiter.api. Вона вказує рушію Jupiter, що даний метод є повноцінним тестовим кейсом і має бути виконаний автоматично. Я завжди звертаю увагу на те, щоб метод повертав void і не мав зайвих параметрів, окрім випадків використання спеціальних розширень. Також важливо слідкувати за правильним імпортом, адже сплутавши його з JUnit 4, можна витратити час на пошук проблеми, чому тести не запускаються. На практиці я щодня використовую цю анотацію для покриття бізнес-логіки моїх автоматизованих тестів.
🗣 How to say it out loudThe main annotation used to mark a test method in JUnit 5 is @Test, which is imported from the org.junit.jupiter.api package. It tells the Jupiter engine that the given method is a fully functional test case and should be executed automatically. I always make sure the method returns void and has no extra parameters, unless special extensions are being used. It's also vital to track the imports correctly, because confusing it with JUnit 4 can lead to wasting time figuring out why tests aren't running. In practice, I use this annotation daily to cover the business logic in my automated tests.
12 Яка анотація JUnit5 виконується перед кожним тестом? Which JUnit5 annotation runs before every test?
JUnit5 та TestNG фреймворки JUnit5 and TestNG frameworks junior ▶

Що це — У фреймворку JUnit5 для виконання певного блоку коду перед кожним окремим тестовим методом використовується анотація @BeforeEach. Вона є прямим аналогом анотації @Before з попередньої версії JUnit4, проте має більш гнучку архітектуру та розширені можливості в рамках платформи JUnit 5 (JUnit Jupiter).

What is it — In the JUnit5 framework, the @BeforeEach annotation is used to execute a specific block of code before every single test method. It is the direct equivalent of the @Before annotation from the previous JUnit4 version, but with a more flexible architecture and enhanced capabilities within the JUnit 5 platform (JUnit Jupiter).

Навіщо/коли — Механізм роботи цієї анотації базується на життєвому циклі тестового класу. За замовчуванням JUnit створює новий екземпляр тестового класу для кожного окремого методу (режим життєвого циклу PER_METHOD). Перед запуском будь-якого методу з анотацією @Test фреймворк автоматично знаходить метод, позначений @BeforeEach, і виконує його. Це гарантує належну ізоляцію тестів: стан об'єктів створюється або скидається наново, запобігаючи впливу побічних ефектів попередніх тестів на поточний.

Why/when — The mechanism of this annotation is based on the test class lifecycle. By default, JUnit creates a new instance of the test class for each individual method (PER_METHOD lifecycle mode). Before running any method annotated with @Test, the framework automatically finds the method marked with @BeforeEach and executes it. This guarantees proper test isolation: the object state is created or reset anew, preventing side effects from previous tests from affecting the current one.

Приклад з проєкту — На практиці @BeforeEach активно застосовується для ініціалізації загальних тестових даних, створення екземплярів тестованих класів, налаштування моків (mock objects) або відкриття сесій у веб-драйвері для UI-тестів. Проте її не слід використовувати для виконання важких, ресурсомістких операцій, які не змінюються під час перевірок (наприклад, запуск контейнерів бази даних чи ініціалізація важких клієнтів) — для таких цілей краще підходить анотація @BeforeAll.

Project example — In practice, @BeforeEach is actively used to initialize common test data, create instances of classes under test, set up mock objects, or open sessions in a web driver for UI tests. However, it should not be used for heavy, resource-intensive operations that do not change during verifications (such as starting database containers or initializing heavy clients)—for such purposes, the @BeforeAll annotation is a better fit.

Підводні камені — Найпоширенішою помилкою серед початківців є спроба зробити метод @BeforeEach статичним (static). На відміну від JUnit4, де методи налаштування часто вимагали статичності у певних сценаріях, у JUnit5 методи @BeforeEach зазвичай не повинні бути статичними, оскільки вони виконуються на рівні екземпляра конкретного тесту. Також помилкою є створення прихованих залежностей між самими тестовими методами замість того, щоб повністю покладатися на @BeforeEach для підтримання чистоти середовища.

Pitfalls — The most common mistake among beginners is trying to make the @BeforeEach method static. Unlike JUnit4, where setup methods often required static modifiers in certain scenarios, in JUnit5, @BeforeEach methods generally should not be static because they execute at the level of the specific test instance. Another mistake is creating hidden dependencies between test methods themselves instead of fully relying on @BeforeEach to maintain a clean environment.

Уточнення на співбесіді — Що буде, якщо @BeforeEach кине виняток? Тест не виконається і буде позначений як failed, а @AfterEach усе одно запуститься для прибирання.

Interview follow-up — What happens if @BeforeEach throws? The test does not run and is marked failed, while @AfterEach still runs to clean up.

Приклад використання анотації @BeforeEach у JUnit5
Example of using @BeforeEach annotation in JUnit5
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class ShoppingCartTest {

    private ShoppingCart cart;

    @BeforeEach
    void setUp() {
        // Виконується перед кожним тестовим методом
        cart = new ShoppingCart();
        cart.addItem("Item1", 100.0);
    }

    @Test
    void testCalculateTotal() {
        assertEquals(100.0, cart.getTotal());
    }

    @Test
    void testClearCart() {
        cart.clear();
        assertTrue(cart.isEmpty());
    }
}
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class ShoppingCartTest {

    private ShoppingCart cart;

    @BeforeEach
    void setUp() {
        // Runs before every test method
        cart = new ShoppingCart();
        cart.addItem("Item1", 100.0);
    }

    @Test
    void testCalculateTotal() {
        assertEquals(100.0, cart.getTotal());
    }

    @Test
    void testClearCart() {
        cart.clear();
        assertTrue(cart.isEmpty());
    }
}
💡 Простими словами: Анотація @BeforeEach працює як особистий помічник перед кожним екзаменом — вона щоразу готує для вас чистий стіл та необхідні інструменти, щоб результати попереднього завдання жодним чином не вплинули на поточне.
💡 In plain words: The @BeforeEach annotation works like a personal assistant before every exam—it cleans the desk and sets up the necessary tools every single time so that the results of the previous task never affect the current one.
🗣 Як читати вголос Для виконання підготовчих дій перед кожним тестовим методом у JUnit5 я завжди використовую анотацію @BeforeEach. Вона гарантує, що допоміжний код — наприклад, ініціалізація об'єктів або створення тестового середовища — виконується ізольовано перед кожним окремим запуском тесту. Це критично важливо для забезпечення незалежності перевірок, щоб побічні ефекти від попереднього тесту не зламали наступний. На практиці я найчастіше застошую її для створення свіжого екземпляра тестованого класу або скидання моків. Головне пам'ятати, що такий метод не повинен бути статичним, адже він працює на рівні екземпляра конкретного тесту.
🗣 How to say it out loud To execute setup actions before every test method in JUnit5, I always use the @BeforeEach annotation. It guarantees that helper code—such as object initialization or test environment setup—runs in isolation before each individual test execution. This is critical for ensuring test independence so that side effects from a previous test do not break the next one. In practice, I most often apply it to create a fresh instance of the class under test or reset mocks. The main thing to remember is that such a method should not be static, as it operates at the level of the specific test instance.
13 Яка анотація JUnit5 виконується один раз перед усіма тестами класу? Which JUnit5 annotation runs once before all tests in a class?
JUnit5 та TestNG фреймворки JUnit5 and TestNG frameworks junior ▶

Що це — У JUnit 5 анотація @BeforeAll використовується для виконання методу рівно один раз перед запуском усіх тестових методів у конкретному класі. Це ідеальне місце для глобальної ініціалізації середовища тестування або підготовки ресурсу, який спільно використовується всіма тестами.

What is it — In JUnit 5, the @BeforeAll annotation is used to execute a method exactly once before any test methods in a specific class are run. It is the ideal place for global test environment initialization or preparing a resource shared across all tests.

Навіщо/коли — Технічно за замовчуванням JUnit створює новий екземпляр тестового класу для кожного окремого методу (життєвий цикл PER_METHOD), щоб гарантувати повну ізоляцію тестів. Через це метод @BeforeAll обов'язково має бути оголошеним як static, оскільки він виконується ще до створення будь-якого об'єкта класу.

Why/when — Technically, by default JUnit creates a new instance of the test class for each individual method (the PER_METHOD lifecycle) to ensure complete test isolation. Because of this, the @BeforeAll method must be declared as static since it executes before any class object is instantiated.

Приклад з проєкту — На практиці в автоматизації тестування (наприклад, із Selenium, Playwright чи RestAssured) цю анотацію найчастіше застосовують для виконання "важких" операцій: запуску локального сервера, підключення до тестової бази даних, ініціалізації конфігураційних файлів чи налаштування глобальних клієнтів API. Це дозволяє уникнути дублювання ресурсів і значно прискорює загальний час виконання тестового набору.

Project example — In practice within test automation (such as with Selenium, Playwright, or RestAssured), this annotation is most commonly used for heavy operations: starting a local server, connecting to a test database, initializing configuration files, or setting up global API clients. This avoids resource duplication and significantly speeds up the overall test suite execution time.

Підводні камені — Поширеною помилкою є спроба змінити стан даних або налаштувати специфічні параметри конкретного тесту всередині @BeforeAll методу, що може призвести до тісного зв'язування тестів та появи "flaky tests". Також типовим багом є забуте ключове слово static, через яке JUnit під час старту викине помилку конфігурації.

Pitfalls — A common mistake is trying to modify data state or configure specific parameters for a particular test inside a @BeforeAll method, which can lead to tightly coupled tests and flaky test behavior. Another typical bug is forgetting the static keyword, causing JUnit to throw a configuration error at startup.

Уточнення на співбесіді — Чому @BeforeAll має бути static? Бо JUnit5 за замовчуванням створює новий екземпляр класу на кожен тест; з @TestInstance(PER_CLASS) static не обов'язковий.

Interview follow-up — Why must @BeforeAll be static? Because JUnit5 creates a new class instance per test by default; with @TestInstance(PER_CLASS) static is not required.

Приклад використання анотації @BeforeAll в JUnit 5
Example of using @BeforeAll annotation in JUnit 5
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

public class DatabaseIntegrationTest {

    @BeforeAll
    public static void setUpClass() {
        // Виконується один раз перед усіма тестами класу
        // Наприклад, підключення до тестової БД або старт середовища
        System.out.println("--> Global setup: Database connection established.");
    }

    @Test
    public void testFirstQuery() {
        System.out.println("Executing testFirstQuery...");
    }

    @Test
    public void testSecondQuery() {
        System.out.println("Executing testSecondQuery...");
    }
}
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

public class DatabaseIntegrationTest {

    @BeforeAll
    public static void setUpClass() {
        // Runs once before all tests in the class
        // For example, connecting to the test DB or starting the environment
        System.out.println("--> Global setup: Database connection established.");
    }

    @Test
    public void testFirstQuery() {
        System.out.println("Executing testFirstQuery...");
    }

    @Test
    public void testSecondQuery() {
        System.out.println("Executing testSecondQuery...");
    }
}
💡 Простими словами: Анотація @BeforeAll працює як кнопка глобального ввімкнення важкого обладнання в лабораторії перед початком серії експериментів. Вона запускає базові налаштування лише один раз на весь клас, економлячи час виконання тестів і захищаючи систему від зайвих перезавантажень.
💡 In plain words: The @BeforeAll annotation acts like a master power switch for heavy lab equipment before starting a series of experiments. It runs setup routines just once per entire class, saving execution time and protecting the system from redundant reloads.
🗣 Як читати вголос Для виконання певної підготовчої логіки лише один раз перед усіма тестами в межах одного тестового класу в JUnit 5 я використовую анотацію @BeforeAll. Зазвичай я застосовую її для ресурсомістких операцій, таких як підключення до бази даних чи ініціалізація драйвера, щоб не витрачати час перед кожним окремим тестом. Важливо пам'ятати, що за замовчуванням метод із цією анотацією має обов'язково бути статичним, оскільки він виконується ще до створення екземпляра самого класу. Якщо мені потрібно змінити цю поведінку, я можу використати життєвий цикл PER_CLASS, але з @BeforeAll усе одно слід діяти обережно. Головне правило тут — не використовувати цей метод для зміни глобального тестового стану, щоб тести залишалися повністю незалежними.
🗣 How to say it out loud To execute certain preparatory logic only once before all tests within a single test class in JUnit 5, I use the @BeforeAll annotation. I typically apply it for resource-intensive operations, such as database connections or driver initialization, so I don't waste time repeating it before every single test. It's important to remember that by default, methods with this annotation must be static because they execute before any instance of the class itself is created. If I need to change this behavior, I can use the PER_CLASS lifecycle, but using it with @BeforeAll still requires caution. The golden rule here is not to use this method to modify global test state so that tests remain completely independent.
14 Як пропустити тест у JUnit5? How do you skip a test in JUnit5?
JUnit5 та TestNG фреймворки JUnit5 and TestNG frameworks junior ▶

Що це — У JUnit 5 для пропуску тестів використовується анотація @Disabled. Вона сигналізує тест-рушію (JUnit 5 Jupiter engine), що відповідний тест або цілий клас тестів слід пропустити під час виконання. Механізм працює на етапі виявлення тестів (test discovery), тому пропущені тести навіть не намагаються виконуватися, а фіксуються в звітах як заблоковані або пропущені (skipped), а не як такі, що впали.

What is it — In JUnit 5, tests can be skipped using the @Disabled annotation. It signals the JUnit 5 Jupiter engine to bypass the annotated test method or an entire test class during execution. This mechanism operates during the test discovery phase, meaning skipped tests are not even attempted; instead, they are recorded in execution reports as skipped rather than failed.

Навіщо/коли — Хорошою практикою є передача рядкового аргументу в анотацію, наприклад @Disabled("Pending bug fix in JIRA-1234"), щоб інші члени команди розуміли причину. Окрім безумовного пропуску, JUnit 5 пропонує потужні механізми умовного виконання, такі як @EnabledOnOs, @DisabledIfEnvironmentVariable або кастомні перевірки через @EnabledIf, що дозволяє пропускати тести залежно від середовища чи конфігурації.

Why/when — It is a best practice to pass a string argument into the annotation, such as @Disabled("Pending bug fix in JIRA-1234"), so team members understand the rationale. Beyond unconditional skipping, JUnit 5 offers powerful conditional execution mechanisms like @EnabledOnOs, @DisabledIfEnvironmentVariable, or custom evaluations via @EnabledIf, allowing tests to be skipped dynamically based on the environment or configuration.

Приклад з проєкту — На реальних проєктах @Disabled найчастіше застосовують під час рефакторингу, коли функціональність тимчасово зламана, або для тестів, що залежать від сторонніх сервісів, які наразі недоступні (наприклад, тестовий шлюз оплати лежить). Уявіть ситуацію: реліз за хвилину, а шлюз платежів на стейджинзі впав через деплой; замість падіння всього білд-пайплайну QA може тимчасово задизаблити цей конкретний інтеграційний тест.

Project example — In real-world projects, @Disabled is most commonly applied during refactoring when a feature is temporarily broken, or for tests dependent on external services that are currently down (such as a temporarily offline payment gateway). Imagine a scenario: a release is imminent, but the staging payment gateway goes down due to deployment; instead of failing the entire build pipeline, a QA engineer can temporarily disable this specific integration test.

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

Pitfalls — The most common mistake among beginners is using @Disabled as a permanent fix for flaky tests. Instead of investigating the root cause (such as timing or synchronization issues), tests are simply turned off, creating a false sense of coverage and accumulating technical debt in the automation suite.

Уточнення на співбесіді — Чим @Disabled відрізняється від Assumptions.assumeTrue? @Disabled вимикає тест завжди, а assumption пропускає його лише за умови під час виконання, наприклад на певному середовищі.

Interview follow-up — How does @Disabled differ from Assumptions.assumeTrue? @Disabled always turns a test off, while an assumption skips it only when a runtime condition holds, e.g. on a given environment.

Приклад використання анотацій пропуску у JUnit 5
Example of skip annotations usage in JUnit 5
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;

public class CheckoutTests {

    @Test
    @Disabled("Temporary disabled due to UI redesign in JIRA-5678")
    void testExpressCheckoutButton() {
        // Test logic for checkout
    }

    @Test
    @EnabledOnOs(OS.LINUX)
    void testLinuxSpecificFeature() {
        // Runs only on Linux, skipped on Windows/Mac
    }
}
💡 Простими словами: Щоб тимчасово вимкнути тест у JUnit 5, достатньо додати над ним анотацію @Disabled разом із поясненням причини. Це рятує білд від падінь через тимчасові проблеми в середовищі, але не замінює виправлення реальних багів чи «плаваючих» тестів.
💡 In plain words: To temporarily turn off a test in JUnit 5, you simply add the @Disabled annotation above it along with an explanatory message. This saves builds from failing due to temporary environmental issues, but it should never replace fixing actual bugs or flaky tests.
🗣 Як читати вголос У JUnit 5 для пропуску тестів використовується анотація @Disabled. Я зазвичай додаю її безпосередньо перед тестовим методом і обов'язково вказую причину в лапках, наприклад, посилання на задачу в Jira, щоб команда розуміла контекст. Окрім цього, JUnit 5 підтримує умовні пропуски через анотації на кшталт @EnabledOnOs або @DisabledIf, якщо тест залежить від середовища виконання. Головне правило на проєкті — використовувати @Disabled лише тимчасово, наприклад, під час падіння сторонніх сервісів, а не ховати за ним плаваючі тести.
🗣 How to say it out loud In JUnit 5, you can skip tests using the @Disabled annotation. I usually place it right above the test method and always provide a reason in quotes, such as a Jira ticket reference, so the team understands the context. In addition to this, JUnit 5 supports conditional skips via annotations like @EnabledOnOs or @DisabledIf when a test depends on the execution environment. The golden rule on a project is to use @Disabled only temporarily, such as during third-party service outages, rather than using it to mask flaky tests.
15 Як параметризувати тест у JUnit5? How do you parameterize a test in JUnit5?
JUnit5 та TestNG фреймворки JUnit5 and TestNG frameworks mid ▶

Що це — Параметризація тестів у JUnit 5 — це підхід, який дозволяє виконувати один і той самий тестовий метод кілька разів із різними наборами вхідних даних, використовуючи модуль junit-jupiter-params. Замість класичної анотації @Test застосовується @ParameterizedTest, яка працює в парі з джерелами даних (sources), такими як @ValueSource, @CsvSource, @MethodSource чи @ArgumentsSource. Механізм влаштований так, що тестовий рушій (Test Engine) перед запуском розгортає потік даних і створює окреме виконання тестового методу для кожного елемента масиву чи колекції, повністю повторюючи для кожного з них життєвий цикл тесту (наприклад, виклики @BeforeEach).

What is it — Test parameterization in JUnit 5 is an approach that allows executing the same test method multiple times with different sets of input data, leveraging the junit-jupiter-params module. Instead of the classic @Test annotation, it uses @ParameterizedTest paired with data sources such as @ValueSource, @CsvSource, @MethodSource, or @ArgumentsSource. Under the hood, the test engine unpacks the data stream before execution and spins up a distinct test invocation for each item in the array or collection, cleanly repeating the test lifecycle (such as @BeforeEach hooks) for every individual data set.

Навіщо/коли — Цей підхід є критично важливим для реалізації Data-Driven Testing, особливо коли потрібно протестувати однакову бізнес-логіку з різними граничними значеннями або валідними/невалідними даними. Наприклад, у проєкті електронної комерції я використовував параметризацію для перевірки форми реєстрації користувача, де потрібно було перевірити два десятки різних форматів електронної пошти та вимоги до паролів. Замість того щоб писати 20 копій одного й того самого тесту, я застосував @CsvSource для передачі пари «email — очікуваний результат валідації», що скоротило кількість коду в рази та покращило його підтримку.

Why/when — This approach is critical for implementing Data-Driven Testing, especially when you need to test identical business logic against various boundary values or valid/invalid inputs. For instance, in an e-commerce project, I used parameterization to validate a user registration form where we needed to test two dozen different email formats and password security requirements. Instead of writing 20 copies of the exact same test, I utilized @CsvSource to pass pairs of "email — expected validation result," which dramatically reduced boilerplate code and improved maintainability.

Приклад з проєкту — Проте параметризацію не варто використовувати, якщо сценарії вимагають кардинально різного налаштування тестового середовища (mock-об'єктів, попереднього стану бази даних) або містять геть інші бізнес-кроки. Головними пастками при роботі з JUnit5 є забуте ключове слово static для методів-провайдерів у @MethodSource (що призводить до помилок компіляції або виключень у рантаймі) та неінформативні імена тестів у звітах. Щоб останнього уникнути, завжди варто налаштовувати параметр name в анотації @ParameterizedTest, аби в Allure-звітах чи консолі чітко бачити, який саме набір даних виконувався.

Project example — However, parameterization should be avoided if the scenarios require radically different context setups (mock objects, database pre-states) or involve completely distinct business workflows. Major pitfalls when working with JUnit5 include forgetting the static keyword for data provider methods in @MethodSource (which leads to runtime errors or compilation issues) and uninformative test names in reports. To avoid the latter, you should always configure the name attribute in the @ParameterizedTest annotation, ensuring that Allure reports or console logs clearly show which specific dataset was executed.

Підводні камені: Недостатня ізоляція тестів може призвести до залежності один від одного, що робить автоматизацію flakey та непередбачуваною.

Уточнення на співбесіді — Коли @CsvSource вже не вистачає? Коли потрібні складні об'єкти — тоді @MethodSource, що повертає Stream<Arguments>, або @CsvFileSource для великих таблиць.

Підводні камені (Pitfalls): Insufficient test isolation can lead to inter-dependencies, rendering the automation flaky and unpredictable.

Interview follow-up — When is @CsvSource no longer enough? When you need complex objects — then @MethodSource returning Stream<Arguments>, or @CsvFileSource for large tables.

Приклад параметризованого тесту у JUnit5 за допомогою @CsvSource
Example of a parameterized test in JUnit5 using @CsvSource
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.*;

public class UserRegistrationTest {

    @ParameterizedTest(name = "Run #{index}: email={0}, isValid={1}")
    @CsvSource({
        "test.user@example.com, true",
        "invalid-email-format, false",
        "@missing-username.com, false",
        "user.name@domain.co.uk, true"
    })
    void testEmailValidation(String email, boolean expectedResult) {
        boolean actualResult = UserValidator.isValidEmail(email);
        assertEquals(expectedResult, actualResult, "Email validation failed for: " + email);
    }
}
💡 Простими словами: Параметризація в JUnit5 дозволяє запускати один і той самий тестовий метод кілька разів, підставляючи щоразу нові дані з джерел на кшталт аннотацій чи методів. Це рятує від дублювання коду та допомагає покрити безліч комбінацій значень у межах одного компактного тесту.
💡 In plain words: Parameterization in JUnit5 allows you to run the same test method multiple times while injecting fresh data from sources like annotations or methods each time. This saves you from code duplication and helps cover a multitude of value combinations within a single compact test.
🗣 Як читати вголос Для параметризації тестів у JUnit5 я використовую модуль junit-jupiter-params разом із анотацією @ParameterizedTest. Замість звичайного @Test ми вказуємо цю анотацію та підключаємо джерело даних, наприклад, @ValueSource для простих масивів, @CsvSource для набору пар значень або @MethodSource для складніших об'єктів. Під капотом рушій JUnit перед запуском розгортає цей потік даних і виконує тест окремо для кожного елемента чи рядка. У реальних проєктах це надзвичайно зручно для перевірки валідаційних форм, розрахунків чи граничних значень, оскільки позбавляє від копіювання однакового коду. Головне — не забувати робити методи-провайдери для @MethodSource статичними та гарно налаштовувати назви тестів через параметр name, щоб звіти виглядали інформативно.
🗣 How to say it out loud To parameterize tests in JUnit5, I use the junit-jupiter-params module along with the @ParameterizedTest annotation. Instead of a regular @Test, we apply this annotation and link a data source, such as @ValueSource for simple arrays, @CsvSource for value pairs, or @MethodSource for more complex objects. Under the hood, the JUnit engine unpacks this data stream before execution and runs the test independently for each item or row. In real projects, this is extremely useful for checking validation forms, calculations, or boundary values, as it prevents duplicating identical code. The main things to keep in mind are making provider methods for @MethodSource static and properly configuring test names using the name attribute so that reports remain clear and informative.
16 Що таке @ExtendWith у JUnit5? What is @ExtendWith in JUnit5?
JUnit5 та TestNG фреймворки JUnit5 and TestNG frameworks mid ▶

Що це — Анотація @ExtendWith у JUnit 5 є ключовим елементом сучасної моделі розширень (Extension Model), яка використовується для декларативного підключення кастомної поведінки до тестів або тестових класів. Вона повністю замінює застарілі механізми @RunWith та @Rule з JUnit 4, роблячи архітектуру фреймворку більш модульною та дозволяючи комбінувати кілька незалежних розширень для одного тестового класу.

What is it — The @ExtendWith annotation in JUnit 5 is a core element of the modern Extension Model, used to declaratively attach custom behavior to tests or test classes. It completely replaces the legacy @RunWith and @Rule mechanisms from JUnit 4, making the framework architecture more modular and allowing multiple independent extensions to be combined for a single test class.

Навіщо/коли — Механізм роботи @ExtendWith базується на спеціальних інтерфейсах-колбеках (Extension APIs), таких як BeforeEachCallback, AfterTestExecutionCallback чи ParameterResolver. Коли JUnit 5 сканує тестовий клас із цією анотацією, він створює екземпляр вказаного класу розширення та автоматично викликає його методи у чітко визначених точках життєвого циклу тестів (Test Lifecycle), забезпечуючи гнучке керування середовищем без жорсткого зв’язування через спадкування.

Why/when — The mechanism behind @ExtendWith is based on specialized callback interfaces (Extension APIs) such as BeforeEachCallback, AfterTestExecutionCallback, or ParameterResolver. When JUnit 5 scans a test class with this annotation, it instantiates the specified extension class and automatically invokes its methods at precisely defined points in the test lifecycle, ensuring flexible environment management without tight coupling via inheritance.

Приклад з проєкту — На реальних проєктах автоматизації тестування @ExtendWith найчастіше застосовується для вирішення крос-доменних завдань: інтеграції з веб-драйвером, управління станом бази даних, кастомного логування або автоматичного створення скріншотів чи збору логів при падінні тестів. Наприклад, ви можете створити розширення ScreenshotOnFailureExtension, яке реалізує інтерфейс TestExecutionExceptionHandler, і просто додати його анотацією над базовим класом вашого фреймворку.

Project example — In real test automation projects, @ExtendWith is most commonly applied for cross-cutting concerns: WebDriver integration, database state management, custom logging, or automatically capturing screenshots and logs upon test failures. For instance, you can create a ScreenshotOnFailureExtension implementing the TestExecutionExceptionHandler interface and simply attach it via annotation above your framework's base class.

Підводні камені — Головною помилкою при роботі з розширеннями є порушення безпеки потоків (Thread Safety) під час паралельного запуску тестів (Parallel Execution). Оскільки екземпляри розширень часто керуються самим фреймворком або існують в єдиному екземплярі, збереження мутабельного стану тесту в полях класу розширення призводить до непередбачуваних падінь і «флейкі» тестів. Крім того, варто уникати надмірного ускладнення логіки через створення занадто довгих ланцюжків розширень там, де можна обійтися звичайними викликами методів.

Pitfalls — A major mistake when working with extensions is violating thread safety during parallel test execution. Since extension instances are often managed by the framework or shared, storing mutable test state inside extension class fields leads to unpredictable failures and flaky tests. Additionally, you should avoid overcomplicating logic by creating excessively long extension chains where simple method calls would suffice.

Уточнення на співбесіді — Чим @ExtendWith відрізняється від @RegisterExtension? Перше реєструє розширення декларативно, друге — через поле, коли розширення треба налаштувати в коді.

Interview follow-up — How does @ExtendWith differ from @RegisterExtension? The first registers an extension declaratively, the second through a field when the extension must be configured in code.

Приклад кастомного розширення для обробки помилок та його підключення
Example of a custom error handling extension and its registration
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestExecutionExceptionHandler;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.Test;

public class ScreenshotExtension implements TestExecutionExceptionHandler {
    @Override
    public void handleTestExecutionException(ExtensionContext context, Throwable throwable) throws Throwable {
        String testName = context.getRequiredTestMethod().getName();
        System.out.println("Test failed: " + testName + ". Taking screenshot...");
        // Логіка збереження скріншота у сховище артефактів
        throw throwable; // Обов'язково прокидаємо помилку далі
    }
}

@ExtendWith(ScreenshotExtension.class)
public class LoginTests {
    @Test
    public void testInvalidCredentials() {
        // Тестовий сценарій, який завершиться помилкою
        org.junit.jupiter.api.Assertions.fail("Forced failure");
    }
}
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestExecutionExceptionHandler;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.Test;

public class ScreenshotExtension implements TestExecutionExceptionHandler {
    @Override
    public void handleTestExecutionException(ExtensionContext context, Throwable throwable) throws Throwable {
        String testName = context.getRequiredTestMethod().getName();
        System.out.println("Test failed: " + testName + ". Taking screenshot...");
        // Logic that saves a screenshot to artifact storage
        throw throwable; // Always rethrow the error
    }
}

@ExtendWith(ScreenshotExtension.class)
public class LoginTests {
    @Test
    public void testInvalidCredentials() {
        // A test scenario that will fail
        org.junit.jupiter.api.Assertions.fail("Forced failure");
    }
}
💡 Простими словами: @ExtendWith — це зручний «плагін» для ваших тестів у JUnit 5. Замість того, щоб писати однакову допоміжну логіку налаштування чи обробки помилок у кожному класі, ви виносите її в окремий клас-розширення та підключаєте однією компактною анотацією.
💡 In plain words: @ExtendWith acts as a convenient plug-in for your tests in JUnit 5. Instead of writing boilerplate setup or error-handling logic in every single class, you encapsulate it in a separate extension class and attach it using a single compact annotation.
🗣 Як читати вголосУ JUnit 5 анотація @ExtendWith використовується для підключення кастомних розширень і повністю замінює застарілі механізми @RunWith та @Rule з четвертої версії. Вона дозволяє декларативно додавати до тестових класів різноманітну крос-доменну поведінку, наприклад, інтеграцію з веб-драйвером, управління базою даних чи обробку виключень. Технічно це реалізовано через спеціальні інтерфейси-колбеки, які фреймворк викликає у чітко визначені моменти життєвого циклу тесту. На практиці я активно використовую це для автоматичного створення скріншотів при падінні тестів або ін'єкції параметрів. Головне, про що важливо пам'ятати — це забезпечення потокобезпеки при паралельному запуску тестів, аби розширення не зберігало спільний мутабельний стан.
🗣 How to say it out loudIn JUnit 5, the @ExtendWith annotation is used to plug in custom extensions and completely replaces the legacy @RunWith and @Rule mechanisms from version four. It allows us to declaratively add cross-cutting behavior to test classes, such as WebDriver integration, database management, or exception handling. Technically, this is implemented through special callback interfaces that the framework triggers at precise points in the test lifecycle. In practice, I actively use this for tasks like automatically taking screenshots on test failures or parameter injection. The main thing to keep in mind is ensuring thread safety during parallel test execution so that the extension doesn't store shared mutable state.
17 Як задати порядок виконання тестів у JUnit5? How do you set test execution order in JUnit5?
JUnit5 та TestNG фреймворки JUnit5 and TestNG frameworks mid ▶

Що це — За замовчуванням JUnit 5 виконує тестові методи у випадковому, хоча й детермінованому порядку, заснованому на хеш-кодах методів. Це зроблено навмисно, щоб запобігти появі прихованих залежностей між тестами та змусити розробників писати повністю ізольовані юніт-тести.

What is it — By default, JUnit 5 executes test methods in a randomized yet deterministic order based on their method hash codes. This is done intentionally to prevent hidden dependencies between tests and force developers to write fully isolated unit tests.

Навіщо/коли — Проте на практиці іноді виникає необхідність керувати порядком виконання. Для цього використовується аннотація класу @TestMethodOrder, куди передається стратегія сортування (наприклад, MethodOrderer.OrderAnnotation.class). Після цього кожен метод можна пронумерувати за допомогою аннотації @Order(1), @Order(2) і так далі. Існують й інші стратегії, такі як MethodName (сортування за алфавітом) чи Random (рандомізація).

Why/when — However, in practice, there is sometimes a need to control execution order. This is achieved using the class-level annotation @TestMethodOrder, which accepts a sorting strategy (e.g., MethodOrderer.OrderAnnotation.class). You can then explicitly number each method using the @Order(1), @Order(2) annotations. Other strategies include MethodName (alphabetical sorting) or Random.

Приклад з проєкту — Цей підхід критично важливий під час написання інтеграційних тестів, багатокрокових сценаріїв (End-to-End) або міграційних тестів бази даних, де кожен наступний крок спирається на результат попереднього. Проте його категорично не варто використовувати для звичайних юніт-тестів, оскільки жорсткий порядок руйнує концепцію незалежності тестів і ускладнює їх паралельний запуск.

Project example — This approach is critical when writing integration tests, multi-step end-to-end scenarios, or database migration tests where each subsequent step relies on the previous one's outcome. However, it should strictly be avoided for standard unit tests because fixed ordering breaks test independence and prevents efficient parallel execution.

Підводні камені — У реальному проєкті я стикався з цим під час тестування модуля оформлення замовлення в інтернет-магазині. Тестовий клас перевіряв чітку послідовність: додавання товару в кошик -> застосування промокоду -> введення даних доставки -> фінальна оплата. Використання @TestMethodOrder дозволило вишикувати ці кроки в логічний ланцюжок усередині одного інтеграційного класу без створення громіздких мега-тестів.

Pitfalls — In a real project, I encountered this when testing an e-commerce checkout module. The test class verified a strict sequence: adding an item to the cart -> applying a promo code -> entering shipping details -> final payment. Using @TestMethodOrder allowed me to chain these steps logically inside a single integration test class without resorting to massive, monolithic test methods.

Уточнююче питання — Найпоширеніша помилка при цьому — створення «крихких» тестів через передачу стану між ними. Наприклад, коли перший тест створює запис у базі даних, а другий тест очікує цей запис, але падає, якщо перший тест пропущено або завершився помилкою. Навіть при впорядкуванні краще використовувати спільні ініціалізатори (@BeforeEach) або незалежні фабрики тестових даних, щоб мінімізувати зв'язність.

Follow-up question — The most common mistake when doing this is creating "brittle" tests by passing mutable state between them. For instance, Test 1 creates a database record, and Test 2 relies on it, failing if Test 1 is skipped or fails. Even with ordering, it is best to use shared initializers (@BeforeEach) or independent data factories to minimize tight coupling.

Приклад налаштування порядку тестів у JUnit 5 за допомогою аннотацій
Example of configuring test order in JUnit 5 using annotations
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class CheckoutIntegrationTest {

    @Test
    @Order(1)
    void testAddItemToCart() {
        // Логіка додавання товару
    }

    @Test
    @Order(2)
    void testApplyPromoCode() {
        // Логіка застосування промокоду
    }

    @Test
    @Order(3)
    void testCompleteCheckout() {
        // Логіка оплати та завершення замовлення
    }
}
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class CheckoutIntegrationTest {

    @Test
    @Order(1)
    void testAddItemToCart() {
        // Add-to-cart logic
    }

    @Test
    @Order(2)
    void testApplyPromoCode() {
        // Promo code logic
    }

    @Test
    @Order(3)
    void testCompleteCheckout() {
        // Payment and checkout logic
    }
}
💡 Простими словами: За замовчуванням JUnit 5 запускає тести в непередбачуваному порядку, щоб переконатися в їхній незалежності. Якщо ж для складних інтеграційних сценаріїв вам життєво необхідно виконувати тести послідовно крок за кроком, використовуйте зв'язку аннотацій @TestMethodOrder та @Order.
💡 In plain words: By default, JUnit 5 runs tests in a non-obvious order to ensure they remain independent. If your complex integration scenarios strictly require step-by-step sequential execution, you can enforce it using the @TestMethodOrder and @Order annotation combination.
🗣 Як читати вголос Зазвичай юніт-тести мають бути повністю ізольованими один від одного, тому в JUnit 5 за замовчуванням порядок виконання не гарантується і є недетермінованим. Проте на практиці, особливо під час написання інтеграційних чи системних тестів, часто виникає потреба виконати сценарії послідовно — наприклад, спочатку додати товар у кошик, потім застосувати промокод, а вже потім оформлювати замовлення. Для цього я використовую аннотацію класу @TestMethodOrder, передаючи в неї стратегію сортування MethodOrderer.OrderAnnotation.class, після чого маркую кожен тестовий метод за допомогою @Order. Головне правило, якого я дотримуюся — ніколи не зловживати цим у звичайних юніт-тестах, бо жорсткий порядок руйнує ізольованість і робить тестовий набір вразливим до змін стану.
🗣 How to say it out loud Typically, unit tests should be completely isolated from each other, which is why JUnit 5 deliberately runs tests in a non-guaranteed, randomized order by default. However, in practice—especially when writing integration or end-to-end tests—you often need to execute scenarios sequentially, such as adding an item to the cart first, applying a promo code second, and checking out last. To achieve this, I use the class-level @TestMethodOrder annotation with the MethodOrderer.OrderAnnotation.class strategy, and then explicitly mark each test method using @Order. The main rule I follow is never to overuse this in standard unit tests, because hardcoded ordering breaks test isolation and makes the suite brittle when state changes occur.
18 Що таке @DisplayName у JUnit5? What is @DisplayName in JUnit5?
JUnit5 та TestNG фреймворки JUnit5 and TestNG frameworks junior ▶

Що це — Анотація @DisplayName у JUnit 5 дозволяє розробникам та тестувальникам задавати власні, читабельні назви для тестових класів та методів, які відображаються у звітах та середовищі розробки (IDE). За замовчуванням тестові фреймворки використовують технічні назви методів (наприклад, shouldThrowExceptionWhenAgeIsNegative), але @DisplayName дозволяє перевизначити їх за допомогою звичайного тексту, пробілів, спецсимволів чи навіть емодзі, роблячи звітність зрозумілою для всієї команди.

What is it — The @DisplayName annotation in JUnit 5 allows developers and testers to declare custom, human-readable display names for test classes and methods, which appear in test reports and IDEs. By default, test frameworks rely on technical method names (e.g., shouldThrowExceptionWhenAgeIsNegative), but @DisplayName lets you override them using plain text, spaces, special characters, or even emojis, making reporting accessible to the entire team.

Навіщо/коли — Цей механізм працює на рівні метаданих JUnit 5 під час виявлення тестів (Test Discovery). Коли запускається звітність (наприклад, Surefire у Maven чи інструменти на кшталт Allure), тестовий рушій зчитує цю анотацію та підмінює стандартний рядок ідентифікатора методу на кастомний опис у звіті. На практиці це незамінно під час демонстрації результатів тестування бізнес-аналітикам або Product Owner-ам, коли ми перевіряємо складні сценарії електронної комерції чи фінансових транзакцій і хочемо бачити чіткий опис бізнес-правила замість ламаного camelCase.

Why/when — This mechanism works via JUnit 5 metadata during Test Discovery. When reporting tools (like Maven Surefire or Allure) run, the test engine reads this annotation and swaps the standard method identifier with the custom description in the report. In practice, this is indispensable when demonstrating test results to Business Analysts or Product Owners, especially when verifying complex e-commerce or financial transaction scenarios where a clear business rule description is preferred over broken camelCase.

Приклад з проєкту — Проте важливо дотримуватися балансу та уникати типових помилок. Поширена помилка новачків — дублювати назву методу простою заміною літер на пробіли або писати занадто довгі, розмиті речення, які перевантажують звіт. Крім того, не варто зловживати @DisplayName для кожного маленького юніт-тесту службового класу (наприклад, гетерів/сетерів), адже стандартні технічні назви там є достатніми й швидшими у підтримці. Головне правило — використовувати її там, де бізнес-логіка потребує гарного опису очікуваного результату для стейкхолдерів.

Project example — However, it is important to strike a balance and avoid common pitfalls. A frequent beginner mistake is simply duplicating the method name into words or writing overly long, vague sentences that clutter the report. Furthermore, avoid overusing @DisplayName for every tiny unit test of utility classes (like getters/setters), as standard technical names are sufficient and easier to maintain there. The golden rule is to use it where business logic demands a nice expected outcome description for stakeholders.

Підводні камені: Недостатня ізоляція тестів може призвести до залежності один від одного, що робить автоматизацію flakey та непередбачуваною.

Уточнення на співбесіді — Чи впливає @DisplayName на запуск тестів з командного рядка? Ні, фільтри Maven Surefire працюють з іменами класів і методів, а @DisplayName лише для звітів.

Підводні камені (Pitfalls): Insufficient test isolation can lead to inter-dependencies, rendering the automation flaky and unpredictable.

Interview follow-up — Does @DisplayName affect running tests from the command line? No, Maven Surefire filters use class and method names; @DisplayName is only for reports.

Приклад використання @DisplayName у JUnit5
Example of using @DisplayName in JUnit5
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@DisplayName("Shopping Cart Checkout Service Tests")
class CartCheckoutTest {

    @Test
    @DisplayName("🚫 Should reject checkout when the cart is completely empty")
    void testEmptyCartCheckout() {
        // Test implementation
    }

    @Test
    @DisplayName("💳 Should successfully process payment with a valid promo code")
    void testCheckoutWithValidPromo() {
        // Test implementation
    }
}
💡 Простими словами: @DisplayName — це інструмент, який перетворює технічні назви методів на зрозумілі людські речення у звітах тестів. Це допомагає менеджерам та замовникам читати результати перевірок без необхідності занурюватися у вихідний код.
💡 In plain words: @DisplayName is a tool that turns technical method names into readable human sentences within test reports. It helps managers and stakeholders read verification results without needing to dive into the source code.
🗣 Як читати вголос Особисто я часто використовую @DisplayName у проєктах, особливо коли ми пишемо інтеграційні тести або тестуємо складну бізнес-логіку з використанням BDD-підходу. За замовчуванням тестові фреймворки виводять у звіти технічні назви на кшталт testCheckout_Negative, але це не завжди зрозуміло для QA-лідів чи менеджерів. За допомогою цієї анотації я можу додати гарний опис із емодзі або чітким формулюванням кейсу, який потім чудово виглядає в Allure-звітах. Головне — не зловживати цим для кожного простого юніт-тесту, щоб не займати зайвий час на підтримку надлишкових описів.
🗣 How to say it out loud Personally, I frequently use @DisplayName in projects, especially when writing integration tests or testing complex business logic with a BDD approach. By default, test frameworks output technical names like testCheckout_Negative into reports, which isn't always clear to QA leads or managers. With this annotation, I can add a nice description with emojis or a clear phrasing of the case, which then looks great in Allure reports. The main thing is not to overuse it for every simple unit test to avoid spending extra time maintaining redundant descriptions.
19 Як згрупувати тести у JUnit5? How do you group tests in JUnit5?
JUnit5 та TestNG фреймворки JUnit5 and TestNG frameworks mid ▶

Що це — У JUnit 5 групування тестів реалізовано за допомогою анотації @Tag, яка стала сучасною та гнучкішою заміною концепції груп із TestNG. Замість успадкування від складних базових класів чи громіздких XML-файлів конфігурації, розробники та QA-інженери можуть маркувати окремі тестові методи або цілі класи однією чи кількома мітками, що дозволяє легко керувати вибором тестів для виконання.

What is it — In JUnit 5, test grouping is implemented using the @Tag annotation, which serves as a modern and more flexible replacement for the TestNG group concept. Instead of inheriting from complex base classes or relying on bulky XML configuration files, developers and QA engineers can mark individual test methods or entire classes with one or more labels, making it easy to manage test selection for execution.

Навіщо/коли — Цей механізм працює через передачу умов фільтрації безпосередньо у тестовий рушій (JUnit Platform). Під час запуску через середовище розробки (IDE) або системи збирання на кшталт Maven (Surefire плагін) чи Gradle, рушій сканує анотації та виконує лише ті тести, які відповідають заданому виразу включення чи виключення (наприклад, через параметри groups чи includeTags), суттєво оптимізуючи час прогону набору тестів.

Why/when — This mechanism works by passing filtering conditions directly to the test engine (JUnit Platform). When executed via an IDE or build systems like Maven (Surefire plugin) or Gradle, the engine scans the annotations and runs only those tests that match the specified include or exclusion expressions (for example, via includeTags parameters), significantly optimizing test suite execution time.

Приклад з проєкту — У реальних проєктах цей підхід критично важливий для побудови ефективних CI/CD пайплайнів. Наприклад, ми маркуємо швидкі модульні тести тегом @Tag("unit"), а важкі взаємодії з базою даних — @Tag("integration") або @Tag("smoke"), що дозволяє запускати швидкі перевірки на кожен коміт, а повну регресію та інтеграційні сценарії залишати для нічних білдів.

Project example — In real-world projects, this approach is critical for building efficient CI/CD pipelines. For example, we mark fast unit tests with @Tag("unit") and heavy database interactions with @Tag("integration") or @Tag("smoke"), allowing us to run quick checks on every commit while leaving full regression and integration scenarios for nightly builds.

Підводні камені — Цю практик варто застосовувати у будь-яких проєктах середнього та великого масштабу, проте слід уникати надмірного дроблення — створення унікального тегу для кожного окремого методу перетворює конфігурацію на хаос. Найпоширенішою помилкою є припущення друкарських помилок у назвах тегів між кодом тестів та налаштуваннями Maven/Gradle, через що частина критичних тестів може мовчки випадати з прогону.

Pitfalls — This practice should be applied in any medium-to-large-scale project, but you should avoid excessive fragmentation—creating a unique tag for every single method turns the configuration into chaos. A common mistake is making typos in tag names between the test code and Maven/Gradle configurations, which can cause critical tests to silently be omitted from execution.

Уточнення на співбесіді — Як запустити лише тести з тегом smoke у Maven? Через властивість groups у Surefire, наприклад mvn test -Dgroups=smoke, або excludedGroups для виключення.

Interview follow-up — How do you run only smoke-tagged tests in Maven? Through the groups property of Surefire, e.g. mvn test -Dgroups=smoke, or excludedGroups to exclude.

Приклад використання анотації @Tag у JUnit 5
Example of using the @Tag annotation in JUnit 5
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertTrue;

@Tag("fast")
public class UserServiceTest {

    @Test
    @Tag("unit")
    void testUserCreation() {
        assertTrue(true);
    }

    @Test
    @Tag("database")
    @Tag("integration")
    void testUserDatabaseSave() {
        assertTrue(true);
    }
}
💡 Простими словами: Групування в JUnit 5 базується на анотації @Tag. Ви просто ставите мітку на тест чи клас (наприклад, @Tag("smoke")), а потім у налаштуваннях Maven чи Gradle вказуєте, які саме мітки потрібно запускати. Це рятує, коли потрібно швидко перевірити основний функціонал без запуску всіх важких тестів.
💡 In plain words: Grouping in JUnit 5 is based on the @Tag annotation. You simply add a label to a test or class (e.g., @Tag("smoke")), and then specify in your Maven or Gradle settings which exact tags should be executed. This is a lifesaver when you need to quickly check core functionality without running all heavy tests.
🗣 Як читати вголос У JUnit 5 для групування тестів використовується анотація @Tag, яка є гнучкою заміною старим групам із TestNG. Я можу ставити цю анотацію як на окремі тестові методи, так і на цілі класи, позначаючи їх тегами на кшталт "unit", "integration" чи "smoke". Потім під час запуску через Maven, Gradle чи IDE я налаштовую фільтрацію, щоб виконувати виключно потрібні категорії тестів. Це надзвичайно зручно для налаштування CI/CD пайплайнів, наприклад, коли на кожен пуш ми запускаємо лише швидкі unit-тести, а важкі інтеграційні перевірки залишаємо для нічних збірок. Головне — стежити за консистентністю назв тегів у коді та конфігураціях збирача, щоб уникнути ситуацій, коли через звичайну опечатку важлива частина тестів не запускається.
🗣 How to say it out loud In JUnit 5, test grouping is handled using the @Tag annotation, which serves as a flexible replacement for the old TestNG groups. I can apply this annotation to individual test methods or entire classes, labeling them as "unit", "integration", or "smoke". Then, when running tests via Maven, Gradle, or an IDE, I configure filtering to execute only the required categories of tests. This is extremely convenient for setting up CI/CD pipelines—for instance, running only fast unit tests on every push while leaving heavy integration checks for nightly builds. The main thing is to maintain consistency in tag names between the code and build configurations to avoid situations where important tests are skipped due to a simple typo.
20 Що таке assertThrows у JUnit5? What is assertThrows in JUnit5?
JUnit5 та TestNG фреймворки JUnit5 and TestNG frameworks junior ▶

Що це — assertThrows у JUnit5 — це спеціальний метод-утвердження (assertion), який використовується для перевірки того, чи викидає певний шматок коду (зазвичай лямбда-вираз) очікуваний тип винятку (exception). Це ключовий інструмент для тестування негативних сценаріїв, коли програма повинна завадити подальшому виконанню через невалідні вхідні дані, порушення бізнес-правил або помилки стану.

What is it — assertThrows in JUnit5 is a dedicated assertion method used to verify that a specific block of code (typically a lambda expression) throws an expected type of exception. It is a key tool for testing negative scenarios where the program must abort execution due to invalid input data, business rule violations, or state errors.

Навіщо/коли — Механізм роботи базується на перехопленні винятку всередині виконання методу assertThrows. Метод приймає два основних аргументи: клас очікуваного винятку (наприклад, IllegalArgumentException.class) та функціональний інтерфейс Executable (найчастіше у вигляді лямбди () -> service.doSomething()). Якщо код усередині лямбди викидає потрібний виняток, assertThrows успішно завершує тест і навіть повертає об'єкт цього винятку, що дозволяє додатково перевірити його текст (getMessage()). Якщо ж виняток не викидається взагалі або викидається інший тип — тест падає.

Why/when — Its mechanism is based on catching the exception during the execution wrapper of assertThrows. The method takes two main arguments: the expected exception class (e.g., IllegalArgumentException.class) and an Executable functional interface (usually as a lambda () -> service.doSomething()). If the code inside the lambda throws the required exception, assertThrows passes successfully and even returns the exception object, allowing you to further assert its text (getMessage()). If no exception or a different type is thrown, the test fails.

Приклад з проєкту — У реальному проєкті автоматизації assertThrows активно застосовується під час тестування валідації даних та бізнес-логіки. Наприклад, коли ми перевіряємо, чи викине метод BankAccount.withdraw() виняток InsufficientFundsException, якщо користувач намагається зняти суму, що перевищує його баланс. Без цього методу довелося б писати громіздкі конструкції try-catch із ручним викликом fail(), що робить код тестів менш читабельним та складнішим у підтриманні.

Project example — In a real automation project, assertThrows is heavily used when testing data validation and business logic. For instance, we verify whether the BankAccount.withdraw() method throws an InsufficientFundsException when a user tries to withdraw an amount exceeding their balance. Without this method, developers would have to write cumbersome try-catch blocks with manual fail() calls, making the test code less readable and harder to maintain.

Підводні камені — Поширеною помилкою серед початківців є перевірка занадто загальних класів винятків, таких як загальний Exception.class або Throwable.class, замість конкретних кастомних класів помилок. Це знижує надійність тесту, оскільки тест може успішно пройти через непередбачуваний баг (наприклад, NullPointerException), а не через очікувану бізнес-перевірку. Також часто забувають використати повернутий об'єкт винятку, щоб заасертити точне повідомлення про помилку.

Pitfalls — A common mistake among beginners is checking for overly broad exception classes like a general Exception.class or Throwable.class instead of specific custom error classes. This reduces test reliability because the test might pass due to an unexpected bug (such as a NullPointerException) rather than the expected business validation. Another common oversight is forgetting to use the returned exception object to assert the exact error message.

Уточнення на співбесіді — Чим assertThrows відрізняється від assertThrowsExactly? Перший приймає і підкласи очікуваного винятку, другий вимагає точного типу.

Interview follow-up — How does assertThrows differ from assertThrowsExactly? The first accepts subclasses of the expected exception, the second demands the exact type.

Приклад використання assertThrows у тест-класі JUnit5
Example of using assertThrows in a JUnit5 test class
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class UserServiceTest {

    @Test
    void shouldThrowExceptionWhenAgeIsInvalid() {
        UserService userService = new UserService();

        IllegalArgumentException exception = assertThrows(
            IllegalArgumentException.class,
            () -> userService.registerUser("John", -5),
            "Expected exception to be thrown, but it wasn't"
        );

        assertEquals("Age cannot be negative", exception.getMessage());
    }
}
💡 Простими словами: assertThrows — це спосіб сказати тестовому фреймворку: «Я очікую, що цей шматок коду зламається з конкретною помилкою. Якщо потрібна помилка сталася — тест пройшов успішно». Це стандартний і найелегантніший підхід для перевірки негативних сценаріїв у тестах.
💡 In plain words: assertThrows is a way to tell the testing framework: "I expect this piece of code to crash with a specific error. If the expected error happens, the test passes successfully." It is the standard and most elegant approach for verifying negative scenarios in tests.
🗣 Як читати вголос Метод assertThrows у JUnit5 використовується спеціально для тестування негативних сценаріїв, коли нам потрібно переконатися, що код правильно реагує на невалідні дані і викидає очікуваний виняток. Він приймає клас винятку та лямбда-вираз із самим кодом, що виконується. Особисто я часто застосовую його на проєкті, наприклад, при валідації вхідних параметрів у сервісах чи контролерах. Головна перевага цього підходу порівняно зі старими конструкціями try-catch полягає в тому, що код тестів стає набагато чистішим і лаконічнішим. Крім того, assertThrows повертає об'єкт самого винятку, що дозволяє мені легко перевірити текст повідомлення про помилку через метод getMessage().
🗣 How to say it out loud The assertThrows method in JUnit5 is specifically used for testing negative scenarios when we need to ensure that the code responds correctly to invalid data and throws the expected exception. It takes the exception class and a lambda expression containing the code under execution. Personally, I frequently use it on my projects, for example, when validating input parameters in services or controllers. The main advantage of this approach compared to old try-catch constructs is that the test code becomes much cleaner and more concise. Moreover, assertThrows returns the exception object itself, which allows me to easily verify the error message text using the getMessage() method.
21 Яка анотація TestNG позначає тест-метод? Which TestNG annotation marks a test method?
JUnit5 та TestNG фреймворки JUnit5 and TestNG frameworks junior ▶

Що це — Анотація @Test у фреймворку TestNG є головним маркером, який повідомляє тест-раннеру, що конкретний Java-метод є тестовим сценарієм і підлягає виконанню. Без цієї анотації TestNG просто проігнорує метод під час запуску звітів чи тест-сьюту (test suite), оскільки сприйматиме його як звичайну службову або допоміжну функцію класу.

What is it — The @Test annotation in the TestNG framework is the primary marker that tells the test runner a specific Java method is a test scenario and should be executed. Without this annotation, TestNG will simply ignore the method during execution of a test suite, treating it as an ordinary utility or helper function.

Навіщо/коли — Під капотом TestNG використовує механізм рефлексії (Reflection API) для сканування класів під час старту тестів. Коли фреймворк знаходить методи, позначені @Test, він автоматично додає їх до черги виконання, керуючи їхнім життєвим циклом у поєднанні з іншими анотаціями на кшталт @BeforeMethod чи @AfterMethod.

Why/when — Under the hood, TestNG uses Java Reflection API to scan classes during test startup. When the framework encounters methods marked with @Test, it automatically queues them for execution, managing their lifecycle in conjunction with setup/teardown annotations like @BeforeMethod or @AfterMethod.

Приклад з проєкту — Цю анотацію варто використовувати на будь-яких публічних методах, які перевіряють певну бізнес-логіку або функціональність продукту. Вона також надає потужні додаткові атрибути, такі як priority (пріоритет виконання), dependsOnMethods (залежність від інших тестів), enabled = false (тимчасове вимкнення) та expectedExceptions (очікувані винятки).

Project example — This annotation should be used on any public methods that verify specific business logic or product functionality. It also provides powerful optional attributes such as priority (execution order), dependsOnMethods (test dependencies), enabled = false (temporary disabling), and expectedExceptions (handling expected errors).

Підводні камені — У реальному проєкті з автоматизації (наприклад, із використанням Selenium WebDriver) ми створюємо класи на зразок CheckoutTests, де кожен окремий сценарій покупки — від додавання товару до оплати — оформлюється як окремий метод із @Test. Це дозволяє легко запускати тести паралельно, групувати їх та отримувати детальні звіти у форматі TestNG Reports або Allure.

Pitfalls — In a real automation project (for instance, using Selenium WebDriver), we create classes like CheckoutTests, where each distinct purchasing scenario—from adding an item to payment—is structured as a separate method annotated with @Test. This allows for easy parallel execution, grouping, and generating detailed reports via TestNG or Allure.

Уточнююче питання — Типовою помилкою новачків є спроба зробити тестові методи private або забуття того, що TestNG вимагає від методів з @Test бути публічними (public), аби механізм рефлексії міг успішно їх викликати. Інша помилка — плутати атрибути TestNG з аналогами з JUnit 5, що призводить до помилок компіляції.

Follow-up question — A common beginner mistake is making test methods private or forgetting that TestNG requires methods marked with @Test to be public so that the reflection mechanism can successfully invoke them. Another mistake is mixing up TestNG attributes with JUnit 5 equivalents, which leads to compilation errors.

Приклад використання анотації @Test з атрибутами у TestNG
Example of using the @Test annotation with attributes in TestNG
import org.testng.annotations.Test;
import org.testng.Assert;

public class LoginTests {

    @Test(priority = 1, description = "Verify successful login with valid credentials")
    public void testValidLogin() {
        boolean isDashboardDisplayed = true; // automation logic
        Assert.assertTrue(isDashboardDisplayed, "Dashboard should be visible after login");
    }

    @Test(priority = 2, expectedExceptions = IllegalArgumentException.class)
    public void testInvalidLoginThrowsException() {
        throw new IllegalArgumentException("Invalid username or password");
    }
}
💡 Простими словами: Анотація @Test — це своєрідний «зелений світ» для фреймворку TestNG. Побачивши її над методом, TestNG розуміє, що це самостійний тест, який треба виконати, перевірити та зафіксувати у загальному звіті про тестування.
💡 In plain words: The @Test annotation acts as a "green light" for the TestNG framework. Upon seeing it above a method, TestNG understands that this is an independent test to be executed, verified, and recorded in the final test report.
🗣 Як читати вголос Для позначення тестового методу в TestNG використовується спеціальна анотація @Test. Вона повідомляє фреймворку, що цей конкретний метод є тестовим сценарієм і його потрібно виконати під час запуску тестів. На відміну від JUnit, TestNG має дуже потужні вбудовані атрибути всередині цієї анотації, такі як priority для керування чергою виконання або expectedExceptions для перевірки очікуваних винятків. На практиці я активно використовую її у своїх проєктах з автоматизації, де кожен тест логічно відокремлений у публічний метод класа. Головне пам'ятати, що метод обов'язково має бути публічним, інакше TestNG просто не зможе викликати його через механізм рефлексії.
🗣 How to say it out loud To mark a test method in TestNG, we use the @Test annotation. It tells the framework that a specific method is a test scenario and needs to be executed during the test run. Unlike JUnit, TestNG provides powerful built-in attributes inside this annotation, such as priority for execution order or expectedExceptions for handling expected errors. In practice, I actively use it in my automation projects where each test case is neatly isolated into a public class method. A key thing to remember is that the method must be public, otherwise TestNG won't be able to invoke it via reflection.
22 Як задати групи тестів у TestNG? How do you define test groups in TestNG?
JUnit5 та TestNG фреймворки JUnit5 and TestNG frameworks mid ▶

Що це — Групи тестів у TestNG задаються за допомогою параметра groups в аннотації @Test на рівнях класів або окремих методів, а також визначаються у файлі конфігурації testng.xml. Механізм працює завдяки тому, що TestNG сканує метадані тестів під час виконання та фільтрує їх, запускаючи лише ті методи, які належать до явно вказаних у конфігурації груп чи їх комбінацій. Це дозволяє гнучко керувати обсягом тестового покриття без необхідності змінювати код самих тестів або створювати складну ієрархію папок.

What is it — Test groups in TestNG are defined using the groups parameter in the @Test annotation at the class or method level, and are configured inside the testng.xml file. The mechanism works by having TestNG scan test metadata at runtime and filter them, executing only those methods that belong to the explicitly specified groups or their combinations. This allows for flexible management of test coverage without having to alter the test code itself or create a complex folder hierarchy.

Навіщо/коли — Їх доцільно використовувати для логічного поділу тестів на функціональні категорії, такі як smoke, regression, sanity, або за середовищами виконання (наприклад, staging чи production). Проте не варто зловживати занадто дрібними групами для кожного мікромодуля, оскільки це призводить до роздутого та важкопідтримуваного testng.xml файлу. Краще дотримуватися чіткої та стандартизованої ієрархії міток.

Why/when — They are best used to logically categorize tests into functional groups such as smoke, regression, sanity, or by execution environments (e.g., staging or production). However, you should avoid overusing overly granular groups for every single micro-module, as this leads to a bloated and unmaintainable testng.xml file. It is better to stick to a clear and standardized tagging hierarchy.

Приклад з проєкту — На реальному проєкті ми використовуємо групи тестів для налаштування пайплайнів у CI/CD. Наприклад, швидкі smoke-тести з міткою "smoke" запускаються автоматично на кожен Pull Request і тривають до п'яти хвилин, тоді як повний регресійний набір із міткою "regression" запускається лише щоночі або перед викачуванням на реліз. Це суттєво економить ресурси серверів та прискорює отримання зворотного зв'язку розробниками.

Project example — In a real project, we use test groups to configure pipelines in CI/CD. For instance, quick smoke tests tagged with "smoke" run automatically on every Pull Request and take up to five minutes, whereas the full regression suite tagged with "regression" runs only nightly or before a release deployment. This significantly saves server resources and accelerates feedback for developers.

Підводні камені — Поширена помилка серед автоматизаторів — забувати включати батьківські групи або некоректно налаштовувати композицію груп (group-of-groups) у testng.xml, через що частина критичних тестів мовру пропускається під час прогону. Також інколи трапляються опечатки в назвах груп або розбіжності в регістрі між аннотацією та конфігурацією, що призводить до того, що TestNG виконує порожній набір тестів.

Pitfalls — A common mistake among automation engineers is forgetting to include parent groups or incorrectly configuring group-of-groups composition in testng.xml, which causes some critical tests to be silently skipped during execution. Another issue is occasional typos in group names or case mismatches between the annotation and configuration, resulting in TestNG executing an empty test set.

Уточнення на співбесіді — Як виключити групу з прогону в testng.xml? Через елемент exclude всередині run у блоці groups — наприклад, щоб не запускати slow на PR.

Interview follow-up — How do you exclude a group from a run in testng.xml? With an exclude element inside run in the groups block — for example, to skip slow tests on a PR.

Приклад анотації груп у тестах та їх виклику в testng.xml
Example of test groups annotation and invocation in testng.xml
public class AuthTests {

    @Test(groups = {"smoke", "ui"})
    public void testSuccessfulLogin() {
        // Test implementation
    }

    @Test(groups = {"regression", "database"})
    public void testUserTokenGeneration() {
        // Test implementation
    }
}

<!-- Фрагмент testng.xml -->
<suite name="Smoke Suite">
    <test name="Quick Validation">
        <groups>
            <run>
                <include name="smoke"/>
            </run>
        </groups>
        <classes>
            <class name="com.example.tests.AuthTests"/>
        </classes>
    </test>
</suite>
public class AuthTests {

    @Test(groups = {"smoke", "ui"})
    public void testSuccessfulLogin() {
        // Test implementation
    }

    @Test(groups = {"regression", "database"})
    public void testUserTokenGeneration() {
        // Test implementation
    }
}

<!-- testng.xml fragment -->
<suite name="Smoke Suite">
    <test name="Quick Validation">
        <groups>
            <run>
                <include name="smoke"/>
            </run>
        </groups>
        <classes>
            <class name="com.example.tests.AuthTests"/>
        </classes>
    </test>
</suite>
💡 Простими словами: Групи в TestNG — це як теги або ярлики для ваших тестів (наприклад, як мітки в Jira чи GitLab). Ви просто позначаєте тести словами на кшталт smoke чи regression, а потім у конфігураційному файлі вказуєте фреймворку, які саме мітки потрібно запустити сьогодні.
💡 In plain words: Groups in TestNG act like tags or labels for your tests (similar to labels in Jira or GitLab). You simply tag tests with words like smoke or regression and then instruct the framework in the configuration file which specific tags to run today.
🗣 Як читати вголос У TestNG групи тестів задаються через параметр groups в анотації @Test, де можна вказати одну або кілька міток для класу чи конкретного методу. Я зазвичай використовую їх для розділення тестової бази на функціональні категорії, такі як smoke, regression або інтеграційні тести. Саму логіку запуску я налаштовую у файлі testng.xml за допомогою тегів run, include та exclude. Це дуже зручно, оскільки дозволяє гнучко керувати пайплайнами в CI/CD без необхідності змінювати код тестів під різні типи перевірок. Головне — стежити за консистентністю назв груп, щоб уникнути ситуацій, коли опечатка призводить до пропуску важливих тестів.
🗣 How to say it out loud In TestNG, test groups are defined via the groups parameter in the @Test annotation, where you can specify one or more tags for a class or a specific method. I typically use them to divide the test base into functional categories such as smoke, regression, or integration tests. I configure the execution logic itself in the testng.xml file using run, include, and exclude tags. This is very convenient because it allows flexible management of CI/CD pipelines without altering the test code for different types of checks. The main thing is to keep group naming consistent to avoid situations where a typo leads to skipped tests.
23 Як задати залежність між тестами у TestNG? How do you define a dependency between tests in TestNG?
JUnit5 та TestNG фреймворки JUnit5 and TestNG frameworks mid ▶

Що це — У TestNG залежності між тестами задаються за допомогою атрибутів dependsOnMethods або dependsOnGroups усередині анотації @Test. Це дозволяє чітко вказувати, що певний тест може виконуватися лише після успішного завершення іншого методу чи цілої групи методів.

What is it — In TestNG, dependencies between tests are defined using the dependsOnMethods or dependsOnGroups attributes inside the @Test annotation. This allows you to explicitly specify that a certain test can only execute after the successful completion of another method or a whole group of methods.

Навіщо/коли — Механізм працює на основі побудови графа виконання: якщо базовий (батьківський) тест падає (fails), усі залежні від нього тести автоматично отримують статус SKIP і не виконуються марно. За замовчуванням TestNG гарантує порядок виконання, але цю поведінку можна гнучко налаштовувати, наприклад, використовуючи параметр alwaysRun = true, якщо залежний метод має спрацювати навіть у разі падіння попередника (наприклад, для очищення даних чи закриття сеансу).

Why/when — The mechanism operates by building an execution graph: if the base (parent) test fails, all dependent tests automatically receive the SKIP status and are not executed in vain. By default, TestNG guarantees execution order, but this behavior can be customized, for instance, by using the alwaysRun = true parameter if the dependent method must run even if the predecessor fails (e.g., for data cleanup or session teardown).

Приклад з проєкту — На практиці це найчастіше використовується в інтеграційному або енд-ту-енд (E2E) тестуванні, де кожен наступний бізнес-крок спирається на результат попереднього. Класичний приклад — сценарій в інтернет-магазині: тест реєстрації користувача (testUserRegistration) є обов'язковим попередником для тесту додавання товару в кошик (testAddToCart), який своєю чергою веде до тесту оформлення замовлення (testCheckout).

Project example — In practice, this is most commonly used in integration or end-to-end (E2E) testing, where each subsequent business step relies on the outcome of the previous one. A classic example is an e-commerce scenario: the user registration test (testUserRegistration) is a mandatory prerequisite for adding an item to the cart (testAddToCart), which in turn leads to the checkout test (testCheckout).

Підводні камені — Проте зловживати цією фічею не варто. Головна помилка новачків — вибудовувати довгі ланцюжки залежностей там, де краще застосувати стандартні хуки на кшталт @BeforeMethod або писати повністю ізольовані модульні тести. Жорсткі залежності ускладнюють паралельне виконання тестів у кілька потоків, оскільки порушують принцип незалежності тестових кейсів і можуть призводити до неочікуваних пропусків великих блоків перевірок.

Pitfalls — However, this feature should not be overused. A common mistake among beginners is building long chains of dependencies where it is better to use standard hooks like @BeforeMethod or write fully isolated unit tests. Rigid dependencies complicate parallel test execution across multiple threads because they violate the principle of test case independence and can lead to unexpected skipping of large blocks of checks.

Уточнення на співбесіді — Чому залежності між тестами вважають ризиком? Падіння одного тесту каскадом пропускає інші і ламає паралельний запуск; краще готувати стан через API в setup.

Interview follow-up — Why are dependencies between tests considered a risk? One failure cascades into skipped tests and breaks parallel runs; it is better to prepare state via the API in setup.

Приклад використання залежностей у TestNG
Example of test dependencies usage in TestNG
public class CheckoutFlowTest {
    
    @Test
    public void testUserRegistration() {
        // Логіка реєстрації користувача / User registration logic
        Assert.assertTrue(true, "User registered successfully");
    }

    @Test(dependsOnMethods = {"testUserRegistration"})
    public void testAddToCart() {
        // Виконується тільки якщо реєстрація успішна / Runs only if registration succeeds
        Assert.assertTrue(true, "Item added to cart");
    }

    @Test(dependsOnMethods = {"testAddToCart"}, alwaysRun = true)
    public void testCleanupSession() {
        // Виконається навіть при падінні попередніх тестів / Runs even if previous tests fail
        Assert.assertTrue(true, "Session closed");
    }
}
public class CheckoutFlowTest {
    
    @Test
    public void testUserRegistration() {
        // User registration logic
        Assert.assertTrue(true, "User registered successfully");
    }

    @Test(dependsOnMethods = {"testUserRegistration"})
    public void testAddToCart() {
        // Runs only if registration succeeds
        Assert.assertTrue(true, "Item added to cart");
    }

    @Test(dependsOnMethods = {"testAddToCart"}, alwaysRun = true)
    public void testCleanupSession() {
        // Runs even if previous tests fail
        Assert.assertTrue(true, "Session closed");
    }
}
💡 Простими словами: Залежності у TestNG дозволяють вибудувати ланцюжок тестів "один за одним", де падіння першого кроку автоматично скасовує всі наступні. Це зручно для складних E2E-сценаріїв, але неприпустимо для звичайних юніт-тестів, які зобов'язані бути повністю ізольованими.
💡 In plain words: Dependencies in TestNG let you chain tests one after another, where a failure in the first step automatically cancels all subsequent ones. This is convenient for complex E2E scenarios, but unacceptable for regular unit tests, which must remain fully isolated.
🗣 Як читати вголос Я використовую механізм залежностей у TestNG переважно для інтеграційних та енд-ту-енд сценаріїв, де кожен наступний крок логічно базується на результаті попереднього. Для цього я застосовую атрибут dependsOnMethods або dependsOnGroups в анотації @Test. Якщо базовий тест падає, TestNG автоматично пропускає залежний тест зі статусом SKIP, що рятує нас від каскаду хибно-негативних падінь. Проте я намагаюся не зловживати цим у юніт-тестах, оскільки це порушує їхню ізольованість і ускладнює паралельний запуск. Якщо ж мені потрібно гарантовано виконати фінальну дію незалежно від успіху попередників — наприклад, закрити з'єднання чи очистити тестові дані — я додатково використовую параметр alwaysRun = true.
🗣 How to say it out loud I use the dependency mechanism in TestNG mainly for integration and end-to-end scenarios where each subsequent step logically relies on the result of the previous one. For this, I apply the dependsOnMethods or dependsOnGroups attribute inside the @Test annotation. If the base test fails, TestNG automatically skips the dependent test with a SKIP status, saving us from a cascade of false failures. However, I try not to abuse this in unit tests because it breaks their isolation and complicates parallel execution. If I need a final action to run regardless of the predecessors' success—like closing a connection or cleaning up test data—I explicitly use the alwaysRun = true parameter.
24 Що таке DataProvider у TestNG? What is a DataProvider in TestNG?
JUnit5 та TestNG фреймворки JUnit5 and TestNG frameworks mid ▶

Що це — DataProvider у TestNG — це спеціальна анотація та механізм, який дозволяє передавати набори тестових даних безпосередньо у тестові методи. Це ключовий інструмент для реалізації Data-Driven Testing (тестування на основі даних), що дає змогу виконувати один і той самий тестовий сценарій багаторазово з різними вхідними параметрами без дублювання коду.

What is it — DataProvider in TestNG is a special annotation and mechanism that allows passing sets of test data directly into test methods. This is a key tool for implementing Data-Driven Testing, allowing the execution of the same test scenario multiple times with different input parameters without code duplication.

Навіщо/коли — Механізм роботи полягає у створенні методу, позначеного анотацією @DataProvider, який зазвичай повертає двовимірний масив об'єктів (Object[][]) або ітератор (Iterator<Object[]>). Тестовий метод прив'язується до цього провайдера через атрибут dataProvider = "імені_провайдера". Під час виконання TestNG автоматично викликає тестовий метод окремо для кожного рядка масиву даних, підставляючи значення у відповідні параметри методу.

Why/when — The mechanism involves creating a method marked with the @DataProvider annotation, which typically returns a two-dimensional array of objects (Object[][]) or an iterator (Iterator<Object[]>). The test method binds to this provider using the dataProvider = "provider_name" attribute. During execution, TestNG automatically invokes the test method separately for each row of the data array, passing the values into the corresponding method parameters.

Приклад з проєкту — Цей підхід ідеально підходить для тестування форм із великою кількістю валідацій (реєстрація, авторизація, фільтри пошуку, калькулятори кредитів), де потрібно перевірити десятки комбінацій валідних і невалідних даних. Проте DataProvider не варто використовувати, коли обсяг даних надзвичайно великий (наприклад, тисячі рядків з БД) або коли тестові дані динамічно залежать від результатів виконання інших тестів — у таких випадках краще читати дані з зовнішніх файлів (JSON, CSV, БД) або застосовувати інші паттерни.

Project example — This approach is ideal for testing forms with numerous validations (registration, authorization, search filters, loan calculators) where you need to check dozens of combinations of valid and invalid data. However, DataProvider should not be used when the dataset is extremely large (e.g., thousands of rows from a database) or when test data dynamically depends on the execution results of other tests—in such cases, it is better to read data from external files (JSON, CSV, DB) or use other patterns.

Підводні камені — У реальному проєкті я часто використовую DataProvider для автоматизації тестування форми оформлення замовлення (Checkout). Наприклад, один тестовий метод перевіряє комбінації полів "Країна", "Поштовий індекс" та "Спосіб оплати", щоб гарантувати, що для кожної країни система коректно застосовує податки та блокує невалідні індекси. Завдяки цьому замість написання 15 окремих тестів я маю один гнучкий метод із таблицею тестових сценаріїв.

Pitfalls — In a real project, I frequently use DataProvider to automate checkout form testing. For instance, one test method checks combinations of "Country", "Postal Code", and "Payment Method" fields to ensure that the system correctly applies taxes and blocks invalid postal codes for each country. As a result, instead of writing 15 separate tests, I have a single flexible method with a table of test scenarios.

Уточнююче питання — Серед найпоширеніших помилок при використанні DataProvider можна виділити невідповідність кількості параметрів у тестовому методі та кількості елементів у масиві даних (що призводить до TestNGException), захаращення тестового класу жорстко закодованими масивами великого розміру, а також забування про ввімкнення паралельного виконання (parallel = true), через що великі набори даних виконуються послідовно і сповільнюють загальний час прогону збірки.

Follow-up question — Common mistakes when using DataProvider include a mismatch between the number of parameters in the test method and the number of elements in the data array (leading to a TestNGException), cluttering the test class with large hardcoded arrays, and forgetting to enable parallel execution (parallel = true), causing large datasets to run sequentially and slow down the overall pipeline execution time.

Приклад використання @DataProvider у TestNG
Example of using @DataProvider in TestNG
@DataProvider(name = "loginDataProvider")
public Object[][] getLoginData() {
    return new Object[][] {
        {"standard_user", "secret_sauce", true},
        {"locked_out_user", "secret_sauce", false},
        {"invalid_username", "secret_sauce", false},
        {"standard_user", "wrong_password", false}
    };
}

@Test(dataProvider = "loginDataProvider")
public void testUserLogin(String username, String password, boolean expectedSuccess) {
    LoginPage loginPage = new LoginPage(driver);
    loginPage.enterUsername(username);
    loginPage.enterPassword(password);
    loginPage.clickLoginButton();
    
    if (expectedSuccess) {
        Assert.assertTrue(driver.getCurrentUrl().contains("/inventory.html"), "Login should succeed");
    } else {
        Assert.assertTrue(loginPage.isErrorMessageDisplayed(), "Error message should be displayed");
    }
}
💡 Простими словами: DataProvider — це вбудований у TestNG генератор даних для тестів. Замість копіювання одного й того ж коду для різних значень, ви створюєте «таблицю» даних, і фреймворк автоматично запускає ваш тест для кожного рядка цієї таблиці окремо.
💡 In plain words: A DataProvider is a built-in test data generator in TestNG. Instead of copying the same test code for different values, you create a data "table," and the framework automatically runs your test for each row of that table separately.
🗣 Як читати вголос DataProvider у TestNG — це чудовий інструмент для реалізації data-driven тестування, який дозволяє відокремити тестові дані від самої логіки перевірки. Він повертає двовимірний масив об'єктів, а тестовий метод звертається до нього за допомогою спеціального атрибута. Я зазвичай використовую цей підхід, коли треба перевірити одну й ту саму форму, наприклад авторизацію чи калькулятор, з десятками різних комбінацій вхідних даних. Головна перевага полягає в тому, що TestNG виконує тест для кожного рядка даних незалежно, а також дозволяє легко розпаралелити цей процес. Головне — слідкувати за кількістю параметрів, щоб уникнути помилок під час мапінгу даних.
🗣 How to say it out loud A DataProvider in TestNG is a great feature for implementing data-driven testing, allowing you to separate test data from the verification logic itself. It returns a 2D object array, and the test method binds to it using a dedicated attribute. I typically use this approach when I need to test the same form, like login or a calculator, with dozens of different input combinations. The main advantage is that TestNG executes the test for each data row independently and allows you to easily parallelize the process. The key is to watch out for parameter counts to avoid mapping errors during runtime.
25 Як підключити DataProvider до тест-методу у TestNG? How do you attach a DataProvider to a test method in TestNG?
JUnit5 та TestNG фреймворки JUnit5 and TestNG frameworks mid ▶

Що це — Для підключення DataProvider до тестового методу у TestNG використовується однойменна анотація @DataProvider, яка зв'язується з тест-методом через параметр dataProvider в анотації @Test. Механізм полягає в тому, що метод-постачальник даних повертає двовимірний масив об'єктів (Object[][]) або ітератор (Iterator), який TestNG автоматично розгортає і передає в якості аргументів до тест-методу під час кожної ітерації виконання.

What is it — To attach a DataProvider to a test method in TestNG, you use the @DataProvider annotation along with the dataProvider attribute inside the @Test annotation. The underlying mechanism works by having the data provider method return a two-dimensional array of objects (Object[][]) or an Iterator, which TestNG automatically iterates over, executing the test method once for each set of parameter data.

Навіщо/коли — Цей підхід є галузевим стандартом для реалізації Data-Driven Testing (DDT), коли один і той самий сценарій потрібно перевірити на десятках різних наборів вхідних даних. Наприклад, у проєкті електронної комерції ви можете написати єдиний тест для перевірки форми реєстрації користувача, передаючи до нього комбінації валідних та невалідних електронних адрес, паролів і граничних значень, що суттєво економить час порівняно з написанням дублюючого коду.

Why/when — This approach is the industry standard for implementing Data-Driven Testing (DDT), where a single test scenario needs to be validated against dozens of different input data combinations. For instance, in an e-commerce project, you can write a single test to verify a user registration form by feeding it various combinations of valid and invalid emails, passwords, and boundary values, which saves significant time compared to writing duplicate code.

Приклад з проєкту — Якщо DataProvider знаходиться в тому ж тестовому класі, достатньо просто вказати його ім'я. Проте у великих автоматизованих фреймворках постачальники даних зазвичай виносяться в окремі класи-утиліти для повторного використання. У такому разі в анотації @Test потрібно додатково передати атрибут dataProviderClass = ExternalDataClass.class, щоб TestNG знав, де саме шукати джерело даних.

Project example — If the DataProvider is in the same test class, you simply specify its name. However, in large automation frameworks, data providers are typically extracted into separate utility classes for reusability. In such cases, you must also pass the dataProviderClass = ExternalDataClass.class attribute in the @Test annotation so TestNG knows where to locate the data source.

Підводні камені — Найпоширеніша помилка автоматизаторів-початківців — невідповідність кількості та типів параметрів у методі DataProvider і тест-методі, що призводить до TestNGException під час запуску тестів. Інша типова проблема полягає у спробі передати змінні некоректних типів без належного приведення (casting), або некоректному управлінні станом даних при паралельному запуску тестів (parallel = true), якщо постачальник використовує спільні змінні.

Pitfalls — A very common mistake among beginner automation engineers is a mismatch in the number and types of parameters between the DataProvider method and the test method, which results in a TestNGException at runtime. Another typical issue involves trying to pass incorrect data types without proper casting, or improperly managing data state when parallel execution is enabled (parallel = true) if the provider relies on shared state variables.

Уточнення на співбесіді — Чи можна взяти DataProvider з іншого класу? Так, через атрибут dataProviderClass, але сам метод провайдера тоді має бути static.

Interview follow-up — Can you take a DataProvider from another class? Yes, with the dataProviderClass attribute, but the provider method must then be static.

Приклад використання DataProvider у TestNG
Example of using DataProvider in TestNG
public class RegistrationTests {

    @DataProvider(name = "registrationData")
    public Object[][] getRegData() {
        return new Object[][] {
            { "valid@email.com", "Password123!", true },
            { "invalid-email", "short", false },
            { "", "", false }
        };
    }

    @Test(dataProvider = "registrationData")
    public void testUserRegistration(String email, String password, boolean expectedResult) {
        RegistrationPage page = new RegistrationPage();
        page.enterEmail(email);
        page.enterPassword(password);
        page.submit();
        
        Assert.assertEquals(page.isRegistrationSuccessful(), expectedResult);
    }
}
💡 Простими словами: DataProvider — це зручний інструмент у TestNG, який дозволяє запускати один і той самий тест кілька разів із різними наборами вхідних даних. Ви створюєте метод із двовимірним масивом даних і просто прив'язуєте його до тест-методу через назву в анотації @Test.
💡 In plain words: A DataProvider is a convenient TestNG feature that allows you to run the exact same test multiple times with different sets of input data. You create a method returning a 2D array of data and simply hook it up to your test method using the name inside the @Test annotation.
🗣 Як читати вголос Щоб підключити DataProvider до тест-методу у TestNG, я використовую зв'язку двох анотацій. Спочатку пишу метод, який повертає двовимірний масив об'єктів і помічаю його анотацією @DataProvider. Потім у самому тестовому методі додаю атрибут dataProvider до анотації @Test, вказуючи назву цього постачальника. Якщо дані знаходяться в іншому класі-утиліті, я також прописую параметр dataProviderClass. Це дуже зручно для Data-Driven тестування, оскільки дозволяє не дублювати однакові тести для різних комбінацій вхідних даних. Головне — ретельно слідкувати за тим, щоб кількість і типи параметрів у постачальнику й тест-методі суворо збігалися, інакше TestNG впаде з помилкою під час старту.
🗣 How to say it out loud To attach a DataProvider to a test method in TestNG, I use a combination of two annotations. First, I write a method that returns a two-dimensional array of objects and mark it with the @DataProvider annotation. Then, in the test method itself, I add the dataProvider attribute to the @Test annotation, specifying the name of that provider. If the data resides in a separate utility class, I also configure the dataProviderClass parameter. This approach is extremely useful for Data-Driven Testing because it avoids duplicating identical tests for various input combinations. The main thing to watch out for is that the number and types of parameters in both the data provider and the test method must match strictly, otherwise TestNG will throw a runtime exception.
26 Що таке testng.xml? What is testng.xml?
JUnit5 та TestNG фреймворки JUnit5 and TestNG frameworks mid ▶

Що це — testng.xml — це конфігураційний файл у форматі XML, який використовується у фреймворку TestNG для організації, групування та запуску тестів у вигляді єдиного набору (suite). Він дозволяє керувати порядком виконання тестів, передавати глобальні параметри в середовище тестування, підключати слухачі (listeners) та налаштовувати паралельне виконання без необхідності змінювати вихідний код тестів.

What is it — testng.xml is a configuration file in XML format used in the TestNG framework to organize, group, and execute tests as a single suite. It allows you to manage test execution order, pass global parameters to the test environment, attach listeners, and configure parallel execution without modifying the source code of the tests.

Навіщо/коли — Механізм роботи базується на тому, що TestNG зчитує цей файл під час ініціалізації та будує динамічний план виконання (Execution Plan). Усередині файлу можна ієрархічно структурувати тести: від рівня suite до окремих test, classes та methods. Такий підхід дає змогу повністю відокремити бізнес-логіку автотестів від їхньої конфігурації та запускати різні комбінації наборів даних однією командою.

Why/when — The mechanism works by TestNG reading this file during initialization and building a dynamic execution plan. Inside the file, you can structure tests hierarchically: from the suite level down to individual test, classes, and methods. This approach completely separates test business logic from its configuration and allows launching different test suite combinations with a single command.

Приклад з проєкту — Цей файл є обов'язковим стандартом для середніх та великих проєктів з автоматизації, особливо під час інтеграції тестів у CI/CD-пайплайни (Jenkins, GitLab CI) для запуску регресійних чи крос-браузерних перевірок. Проте його не варто використовувати для локального запуску одиничних юніт-тестів, оскільки в такому разі набагато швидше і простіше запустити окремий клас чи метод безпосередньо з інтегрованого середовища розробки (IDE).

Project example — This file is an essential standard for medium and large automation projects, especially when integrating tests into CI/CD pipelines (Jenkins, GitLab CI) to run regression or cross-browser checks. However, it shouldn't be used for local execution of individual unit tests, as it is much quicker and simpler to run a single class or method directly from the IDE in those cases.

Підводні камені — У реальному проєкті з автоматизації UI-тестів за допомогою Selenium WebDriver testng.xml використовується для конфігурації паралельного запуску тестів у кількох потоках із передачею параметрів браузера через тег <parameter>. Окрім цього, у ньому прописуються кастомні слухачі (Listeners) для автоматичного створення скриншотів у разі падіння тестів та інтеграції зі звітами Allure або TestNG Reports.

Pitfalls — In a real UI automation project using Selenium WebDriver, testng.xml is used to configure parallel test execution across multiple threads while passing browser parameters via the <parameter> tag. Additionally, it specifies custom listeners for automatically capturing screenshots on test failures and integrating with Allure or TestNG reports.

Уточнююче питання — Типовою помилкою серед початківців є дублювання шляхів до класів або пакетів, через що тести можуть запускатися двічі або пропускатися взагалі. Інша критична проблема — ігнорування принципів потокобезпеки (thread safety) при паралельному запуску, коли неізольовані екземпляри драйверів чи сервісів спільно використовуються потоками, що призводить до спорадичних та непередбачуваних падінь тестів (flaky tests).

Follow-up question — A common mistake among beginners is duplicating class or package paths, which can cause tests to run twice or get skipped entirely. Another critical issue is ignoring thread safety principles during parallel execution, where non-isolated driver or service instances are shared across threads, leading to flaky and unpredictable test failures.

Приклад конфігураційного файлу testng.xml з паралельним запуском та слухачами
Example of a testng.xml configuration file with parallel execution and listeners
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Regression Suite" parallel="tests" thread-count="3">
    <parameter name="browser" value="chrome"/>
    <listeners>
        <listener class-name="com.example.utils.AllureListener"/>
        <listener class-name="com.example.utils.AnnotationTransformer"/>
    </listeners>
    <test name="UI Regression Tests">
        <classes>
            <class name="com.example.tests.LoginTest"/>
            <class name="com.example.tests.CheckoutTest"/>
        </classes>
    </test>
</suite>
💡 Простими словами: testng.xml — це пульт керування вашими автотестами. Замість того щоб запускати кожен клас вручну, ви один раз описуєте структуру у цьому файлі і можете запускати цілі пакети тестів паралельно, міняти браузери або підключати звіти однією командою.
💡 In plain words: testng.xml is the remote control for your automated tests. Instead of running each class manually, you describe the structure once in this file and can run entire test packages in parallel, switch browsers, or attach reporting with a single command.
🗣 Як читати вголос testng.xml — це головний конфігураційний файл у TestNG, який дозволяє керувати тим, як, коли і в якому порядку запускатимуться наші автотести. Я активно використовую його в проєктах для організації регресійних наборів, налаштування паралельного виконання тестів у кілька потоків, а також для передачі глобальних параметрів, таких як середовище чи браузер. Наприклад, у моєму поточному проєкті цей файл допомагає запускати UI-тести паралельно для різних браузерів та підключати Allure-слухачі для звітів. Головне під час роботи з ним — пам'ятати про потокобезпеку, щоб тести не конфліктували між собою при паралельному запуску.
🗣 How to say it out loud testng.xml is the main configuration file in TestNG that allows us to control how, when, and in what order our automated tests run. I actively use it in projects to organize regression suites, configure parallel test execution across multiple threads, and pass global parameters like the environment or browser. For example, in my current project, this file helps run UI tests in parallel for different browsers and attaches Allure listeners for reports. The main thing to remember when working with it is thread safety so that tests don't conflict with each other during parallel execution.
27 Що таке ITestListener у TestNG? What is ITestListener in TestNG?
JUnit5 та TestNG фреймворки JUnit5 and TestNG frameworks senior ▶

Що це — ITestListener у TestNG — це спеціалізований інтерфейс, який дозволяє перехоплювати події життєвого циклу тестів під час їх виконання. Реалізуючи його методи (такі як onTestStart, onTestSuccess, onTestFailure тощо), автоматизатор може втручатися в процес запуску тестів без зміни самих тестових класів. Це робить його ідеальним інструментом для реалізації сквозної функціональності (cross-cutting concerns), як-от звітування, логування або кастомна обробка винятків.

What is it — ITestListener in TestNG is a specialized interface that allows you to hook into the test execution lifecycle events. By implementing its callback methods—such as onTestStart, onTestSuccess, and onTestFailure—automation engineers can intercept test execution without modifying the test classes themselves. This makes it an ideal tool for handling cross-cutting concerns like reporting, logging, or custom exception handling across the entire test suite.

Навіщо/коли — Механізм роботи ґрунтується на патерні "Спостерігач" (Observer): рушій TestNG викликає відповідні методи слухача у момент переходу тестів з одного стану в інший. Зареєструвати ITestListener можна кількома способами: за допомогою анотації @Listeners(MyListener.class) на рівні класу, через конфігураційний файл testng.xml, або програмно за допомогою класу TestNG. Важливо розуміти, що слухачі працюють у контексті потоку виконання тестів, тому при паралельному запуску слід уважно ставитися до потокобезпеки (thread safety).

Why/when — The mechanism relies on the Observer pattern, where the TestNG runner invokes the listener's callback methods as tests transition between states. You can register an ITestListener in several ways: using the @Listeners(MyListener.class) annotation at the class level, via the testng.xml configuration file, or programmatically using the TestNG API. It's crucial to understand that listeners execute within the test thread context, meaning thread safety must be carefully managed when running tests in parallel.

Приклад з проєкту — У реальному проєкті з автоматизації UI-тестів ITestListener є ключовим елементом для інтеграції зі звітами та CI/CD. Наприклад, у методі onTestFailure ми можемо отримати поточний екземпляр вебдрайвера, зробити знімок екрана (screenshot) у форматі Base64, прикріпити його до звіту Allure чи ExtentReports за допомогою виклику Allure.addAttachment(), а також надіслати сповіщення про падіння. Це дозволяє централізовано керувати артефактами збоїв без дублювання коду в кожному з сотень тестів.

Project example — In a real-world UI automation project, an ITestListener is a core element for reporting and CI/CD integration. For instance, inside the onTestFailure method, we can extract the active WebDriver instance, capture a Base64 screenshot, attach it to an Allure or ExtentReports dashboard using Allure.addAttachment(), and even push failure alerts. This enables centralized management of failure artifacts without cluttering hundreds of individual test scripts with repetitive try-catch blocks.

Підводні камені — Найпоширеніша помилка при використанні ITestListener — це збереження стану у не-потокобезпечних змінних (non-thread-safe fields) усередині класу слухача під час паралельного запуску, що призводить до «змішування» даних між потоками та хибних звітів. Крім того, не варто використовувати слухачі для бізнес-логіки тестів або налаштування середовища (setup/teardown) — для цього існують анотації @BeforeMethod та @AfterMethod. Слухачі призначені виключно для спостереження та побічних ефектів.

Pitfalls — A common mistake when using ITestListener is maintaining state in non-thread-safe class fields during parallel execution, which causes data races, cross-contamination, and flaky reporting. Additionally, you should not use listeners for core test business logic, data setup, or teardown tasks—that is the responsibility of @BeforeMethod and @AfterMethod annotations. Listeners are strictly meant for observation and side-effects.

Уточнення на співбесіді — Чим ITestListener відрізняється від IRetryAnalyzer? Listener лише реагує на події тесту, а retry analyzer вирішує, чи перезапускати тест після падіння.

Interview follow-up — How does ITestListener differ from IRetryAnalyzer? A listener only reacts to test events, while a retry analyzer decides whether to rerun a test after a failure.

Приклад реалізації ITestListener для автозвітування та скриншотів
Example of an ITestListener implementation for auto-reporting and screenshots
public class CustomTestListener implements ITestListener {

    @Override
    public void onTestStart(ITestResult result) {
        System.out.println("Test started: " + result.getName());
    }

    @Override
    public void onTestFailure(ITestResult result) {
        System.err.println("Test failed: " + result.getName());
        
        // Example: capturing screenshot and attaching to Allure on failure
        Object currentClass = result.getInstance();
        WebDriver driver = ((BaseTest) currentClass).getDriver();
        if (driver != null) {
            byte[] screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
            Allure.addAttachment("Failure Screenshot", new ByteArrayInputStream(screenshot));
        }
    }

    @Override
    public void onTestSkipped(ITestResult result) {
        System.out.println("Test skipped: " + result.getName());
    }
}
💡 Простими словами: ITestListener — це «слухач», який автоматично слідкує за життєвим циклом ваших тестів. Він дозволяє виконувати певні дії (наприклад, робити скриншот чи логувати помилку) саме в той момент, коли тест починається, падає чи успішно завершується, позбавляючи вас від необхідності писати однаковий код у кожному тесті.
💡 In plain words: ITestListener is an observer that automatically tracks your test lifecycle events. It lets you trigger specific actions—like capturing a screenshot or logging a failure—precisely when a test starts, fails, or succeeds, saving you from writing repetitive boilerplate code inside the tests themselves.
🗣 Як читати вголос ITestListener у TestNG — це спеціальний інтерфейс, який дозволяє перехоплювати події виконання тестів, такі як початок, успіх, падіння чи пропуск. Я активно використовую його у своїх фреймворках для налаштування інтеграції зі звітами, наприклад, для автоматичного створення скриншотів та прикріплення їх до Allure, коли якийсь тест падає. Реєструвати слухача можна через анотацію @Listeners або через файл testng.xml. Головне, про що я завжди пам'ятаю на практиці — це потокобезпека, адже при паралельному запуску тестів усі змінні всередині слухача мають бути thread-safe, щоб уникнути конфліктів даних.
🗣 How to say it out loud ITestListener in TestNG is a powerful interface used to hook into test execution lifecycle events like test start, success, failure, or skip. I actively use it in my frameworks for reporting integration—for instance, to automatically capture screenshots and attach them to Allure reports whenever a test fails. You can register a listener easily via the @Listeners annotation or directly inside the testng.xml configuration file. The key thing I always keep in mind during implementation is thread-safety, because when tests run in parallel, any shared state inside the listener can lead to race conditions and corrupted test reports.
28 Як зробити soft assertions у TestNG? How do you do soft assertions in TestNG?
JUnit5 та TestNG фреймворки JUnit5 and TestNG frameworks mid ▶

Що це — У TestNG стандартні твердження (Hard Assertions через клас Assert) переривають виконання тесту одразу після першої ж помилки. Натомість soft assertions (м'які перевірки) реалізовані через спеціальний клас SoftAssert, який дозволяє тесту продовжити виконання навіть у разі падіння окремих перевірок, збираючи всі збої в пам'яті до кінця тестового методу.

What is it — In TestNG, standard assertions (Hard Assertions via the Assert class) abort test execution immediately upon the first failure. Conversely, soft assertions are implemented using a dedicated SoftAssert class, which allows the test to continue executing even when individual checks fail, collecting all failures in memory until the end of the test method.

Навіщо/коли — Механізм роботи базується на збереженні станів невдалих перевірок всередині інстансу SoftAssert. Щоб тест зрештою впав і повідомив про всі накопичені помилки, ви зобов'язані викликати метод softAssert.assertAll() наприкінці тестового методу. Якщо забути викликати цей метод, тест завершиться зі статусом PASSED, навіть якщо половина перевірок всередині провалилася.

Why/when — The mechanism relies on storing the states of failed checks inside a SoftAssert instance. For the test to ultimately fail and report all accumulated errors, you are required to call the softAssert.assertAll() method at the end of the test method. If you forget to invoke this method, the test will finish with a PASSED status even if half of the internal checks failed.

Приклад з проєкту — У реальних проєктах soft assertions найчастіше застосовуються для перевірки складних UI-форм, сутності профілю користувача або комплексних JSON-відповідей API, де є безліч незалежних полів. Наприклад, перевіряючи сторінку оформлення замовлення, ми хочемо одразу побачити статус індикатора, назву продукту, ціну та знижку одночасно, щоб отримати повну картину дефектів за один прогін.

Project example — In real-world projects, soft assertions are most commonly applied when verifying complex UI forms, user profile entities, or complex JSON API responses containing multiple independent fields. For instance, when verifying a checkout page, we want to instantly see the status of badges, product name, price, and discount simultaneously to get a complete picture of defects in a single run.

Підводні камені — Проте не варто зловживати soft assertions для критичних залежних кроків. Якщо крок авторизації в системі завершився невдачею, подальші перевірки елементів особистого кабінету втрачають будь-який сенс, і тут потрібен саме звичайний hard assert. Головна помилка початківців — масово використовувати soft assert усюди і забувати про фінальний виклик assertAll(), що створює хибне відчуття стабільності у звітах CI/CD.

Pitfalls — However, you should not abuse soft assertions for critical dependent steps. If the authorization step in the system fails, subsequent checks on dashboard elements lose all meaning, and a standard hard assert is required there instead. A major beginner mistake is overusing soft asserts everywhere and forgetting the final assertAll() call, which creates a false sense of stability in CI/CD reports.

Уточнення на співбесіді — Що буде, якщо забути викликати assertAll у SoftAssert? Усі зібрані помилки мовчки загубляться, і тест стане зеленим — класичний хибний pass.

Interview follow-up — What happens if you forget to call assertAll on SoftAssert? All collected failures are silently lost and the test goes green — a classic false pass.

Приклад використання SoftAssert у TestNG
Example of using SoftAssert in TestNG
import org.testng.annotations.Test;
import org.testng.asserts.SoftAssert;

public class UserProfileTest {

    @Test
    public void testUserProfileFields() {
        SoftAssert softAssert = new SoftAssert();

        String actualName = "John Doe";
        String actualEmail = "john.doe@example.com";
        boolean isAvatarDisplayed = false; // Suppose this check fails

        // Soft assertions collect failures instead of stopping execution immediately
        softAssert.assertEquals(actualName, "John Doe", "Name mismatch!");
        softAssert.assertEquals(actualEmail, "john.doe@example.com", "Email mismatch!");
        softAssert.assertTrue(isAvatarDisplayed, "Avatar is missing!");

        // CRITICAL: Must be called at the end to evaluate all accumulated errors
        softAssert.assertAll();
    }
}
💡 Простими словами: Soft assertions дозволяють перевірити кілька речей в одному тесті, навіть якщо перша перевірка провалилася. Головне — не забути наприкінці написати assertAll(), інакше тест пройде успішно попри всі помилки всередині.
💡 In plain words: Soft assertions allow you to check multiple things in a single test even if the first check fails. The key is not to forget writing assertAll() at the end, otherwise the test will pass successfully despite any inner errors.
🗣 Як читати вголос Soft assertions у TestNG використовуються тоді, коли мені потрібно перевірити кілька незалежних елементів в одному тесті, але при цьому я не хочу, щоб тест зупинявся на першій же помилці. Для цього я створюю екземпляр класу SoftAssert, роблю з його допомогою звичні перевірки на кшталт assertEquals чи assertTrue, а в кінці методу обов'язково викликаю assertAll(). Цей метод збирає всі падіння разом і кидає загальний AssertionError, якщо хоча б одна перевірка не пройшла. Це надзвичайно зручно для перевірки великих об'єктів або сторінок із безліччю полів, бо дає повну картину дефектів за один запуск. Але я намагаюся не використовувати це там, де подальші кроки критично залежать від успішності попередніх.
🗣 How to say it out loud Soft assertions in TestNG are used when I need to check several independent elements in a single test without stopping execution on the very first failure. To do this, I instantiate the SoftAssert class, perform standard checks like assertEquals or assertTrue through it, and at the end of the method, I must call assertAll(). This method collects all failures together and throws a combined AssertionError if at least one check fails. It's extremely convenient for verifying large objects or pages with many fields because it provides a complete picture of defects in a single run. However, I avoid using it where subsequent steps critically depend on the success of prior ones.
▶ 3 12
29 Яка команда Maven запускає тести? Which Maven command runs tests?
Збірка, залежності та Git Build, dependencies and Git junior ▶

Що це — Основною командою Maven для запуску тестів є mvn test. Вона викликає відповідну фазу життєвого циклу Maven (lifecycle phase), яка відповідає за компіляцію та виконання модульних тестів у проєкті. За замовчуванням ця команда компілює основний код, компілює тестовий код і запускає всі юніт-тести, знайдені у проєкті, використовуючи зазвичай плагін Maven Surefire Plugin.

What is it — The main Maven command for running tests is mvn test. It triggers the corresponding phase of the Maven build lifecycle, which is responsible for compiling and executing unit tests in the project. By default, this command compiles the main source code, compiles the test code, and executes all unit tests found in the project, typically using the Maven Surefire Plugin.

Навіщо/коли — Механізм цієї команди базується на послідовному виконанні фаз збірки Maven. Коли ви вводите mvn test, Maven спочатку виконує фази компіляції (compile та test-compile), а потім запускає фазу test. Плагін Surefire сканує директорію src/test/java на наявність класів із назвами, що відповідають певним шаблонам (наприклад, закінчуються на Test), і виконує їх через інтегрований тестовий фреймворк, такий як JUnit або TestNG.

Why/when — The mechanism of this command is based on the sequential execution of Maven build phases. When you enter mvn test, Maven first executes the compilation phases (compile and test-compile) and then triggers the test phase. The Surefire plugin scans the src/test/java directory for classes matching specific naming patterns (such as ending with Test) and executes them via an integrated test framework like JUnit or TestNG.

Приклад з проєкту — Цю команду варто використовувати під час локальної розробки для швидкої перевірки працездатності написаного коду перед створенням коміту або пулл-реквесту. Проте її не варто використовувати для запуску інтеграційних тестів, оскільки для цього у Maven передбачена інша фаза та інший плагін (Failsafe Plugin). Також розробники часто використовують додаткові параметри, наприклад, -Dtest=ClassName, щоб запустити лише один конкретний тест замість усього набору.

Project example — This command should be used during local development for a quick sanity check of your code before creating a commit or pull request. However, it should not be used to run integration tests, as Maven provides a separate phase and plugin (the Failsafe Plugin) for that purpose. Developers also frequently use additional flags, such as -Dtest=ClassName, to run a single specific test instead of the entire suite.

Підводні камені — У реальному проєкті з автоматизації тестування на Java ви працюєте з цією командою щодня. Наприклад, після написання нових UI-тестів за допомогою Selenium WebDriver ви відкриваєте термінал у кореневій папці проєкту, де розташований файл pom.xml, і вводите mvn test. У консолі ви бачите процес виконання, кількість успішних тестів, помилки (якщо вони є), а результати та звіти автоматично зберігаються в папці target/surefire-reports.

Pitfalls — In a real-world Java test automation project, you work with this command daily. For example, after writing new UI tests using Selenium WebDriver, you open the terminal in the project's root folder where the pom.xml file is located and type mvn test. In the console, you see the execution progress, the number of passed tests, any failures, and the results and reports are automatically saved in the target/surefire-reports folder.

Уточнююче питання — Поширеною помилкою початківців є спроба запускати інтеграційні тести (які перевіряють взаємодію з БД чи API та часто мають суфікс IT) за допомогою команди mvn test. Оскільки Surefire ігнорує класи з суфіксом IT за замовчуванням, такі тести просто не виконаються, що може ввести розробника в оману. Інша поширена помилка — запуск команди поза кореневою директорією проєкту, що призводить до помилки відсутності файлу pom.xml.

Follow-up question — A common mistake for beginners is attempting to run integration tests (which verify DB or API interactions and often have the IT suffix) using the mvn test command. Since Surefire ignores classes with the IT suffix by default, such tests will simply not execute, which can mislead the developer. Another common mistake is running the command outside the project's root directory, resulting in a missing pom.xml file error.

Приклад виконання команди mvn test у терміналі та її виводу
Example of executing the mvn test command in the terminal and its output
mvn test -Dtest=LoginTest

[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Building ui-automation-framework 1.0.0
[INFO] ------------------------------------------------------------------------
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ ui-automation-framework ---
[INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ ui-automation-framework ---
[INFO] 
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.qa.tests.LoginTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.45 s
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
💡 Простими словами: Команда mvn test — це стандартний спосіб сказати Maven: «Збери мій проєкт і запусти всі модульні тести». Вона економить час QA-інженера, автоматично знаходячи та виконуючи тестові сценарії через плагін Surefire без потреби налаштовувати запуск вручну.
💡 In plain words: The mvn test command is the standard way to tell Maven: "Build my project and run all unit tests." It saves a QA engineer's time by automatically discovering and executing test scenarios via the Surefire plugin without manual setup.
🗣 Як читати вголосДля запуску тестів у проєкті на Maven я використовую команду mvn test. Вона запускає відповідну фазу життєвого циклу Maven, під час якої компілюється код і виконуються юніт-тести через плагін Surefire. Зазвичай я застосовую цю команду локально перед тим, як зробити коміт, щоб швидко перевірити працездатність коду. Якщо ж мені потрібно зекономити час і запустити лише один конкретний тест, я додаю спеціальний параметр -Dtest=ClassName. Важливо пам'ятати, що mvn test запускає саме модульні тести, тоді як для інтеграційних тестів використовується плагін Failsafe та команда mvn verify.
🗣 How to say it out loudTo run tests in a Maven project, I use the mvn test command. It triggers the corresponding Maven lifecycle phase, which compiles the code and executes unit tests via the Surefire plugin. Typically, I run this command locally before making a commit to quickly verify code functionality. If I need to save time and run only one specific test, I add the special parameter -Dtest=ClassName. It's important to remember that mvn test runs unit tests specifically, whereas integration tests require the Failsafe plugin and the mvn verify command.
30 Який Maven плагін запускає TestNG/JUnit тести? Which Maven plugin runs TestNG/JUnit tests?
Збірка, залежності та Git Build, dependencies and Git mid ▶

Що це — Основним Maven-плагіном, який відповідає за запуск модульних (unit) та компонентних тестів на платформах JUnit і TestNG, є maven-surefire-plugin. Цей плагін глибоко інтегрований у стандартний життєвий цикл збірки Maven і автоматично активується на фазі test, скануючи проєкт на наявність тестових класів за замовчуванням або за явно вказаними шаблонами назв (наприклад, *Test.java).

What is it — The primary Maven plugin responsible for executing unit and component tests using JUnit and TestNG frameworks is the maven-surefire-plugin. This plugin is deeply integrated into the standard Maven build lifecycle and automatically triggers during the test phase, scanning the project for test classes based on default naming conventions or explicit patterns (e.g., *Test.java).

Навіщо/коли — Механізм роботи плагіна базується на створенні окремого дочірнього процесу JVM (forking), в якому ізольовано виконуються всі знайдені тести. Така ізоляція захищає процес збірки від неочікуваних падінь, критичних помилок чи витоків пам'яті всередині тестів. Після завершення прогону Surefire формує детальні звіти у форматі XML та текстових файлів у директорії target/surefire-reports, які згодом зчитуються системами CI/CD або інструментами на кшталт Allure.

Why/when — The plugin operates by forking a separate JVM process where all discovered tests run in strict isolation. This isolation protects the main build process from unexpected crashes, critical failures, or memory leaks originating from tests. Once execution finishes, Surefire generates detailed XML and plain text reports inside the target/surefire-reports directory, which are subsequently consumed by CI/CD systems or reporting tools like Allure.

Приклад з проєкту — У реальних проєктах з автоматизації цей плагін використовується щоразу, коли розробники чи QA-інженери запускають команду mvn test локально або в пайплайні. Наприклад, ми налаштовуємо maven-surefire-plugin у файлі pom.xml для передачі спеціальних параметрів, таких як запуск конкретних сюїт через <suiteXmlFiles>, фільтрація за групами TestNG або увімкнення паралельного виконання тестів задля оптимізації часу прогону регресії.

Project example — In real automation projects, this plugin is invoked whenever developers or QA engineers run the mvn test command locally or within a pipeline. For instance, we configure maven-surefire-plugin inside the pom.xml file to pass specific parameters, such as executing specific XML test suites via <suiteXmlFiles>, filtering by TestNG groups, or enabling parallel test execution to optimize regression run time.

Підводні камені — Типовою помилкою є спроба запускати важкі інтеграційні або системні тести (наприклад, ті, що вимагають повністю піднятого вебсервера чи взаємодії з реальною базою даних) безпосередньо через Surefire. Оскільки Surefire обриває життєвий цикл збірки щойно падає хоча б один тест на фазі test, для таких задач краще використовувати сусідній плагін — maven-failsafe-plugin, який призначений саме для фази інтеграційного тестування та гарантує виконання етапу очищення середовища (teardown).

Pitfalls — A common mistake is attempting to run heavy integration or system tests (such as those requiring a fully running web server or real database interactions) directly through Surefire. Because Surefire halts the build lifecycle immediately if any test fails during the test phase, such scenarios require its counterpart—the maven-failsafe-plugin, which is specifically designed for integration testing and ensures proper environment teardown even if tests fail.

Уточнення на співбесіді — Як Surefire вирішує, які класи запускати? За шаблонами імен за замовчуванням: Test*, *Test, *Tests і *TestCase; інші класи потрібно додати через includes.

Interview follow-up — How does Surefire decide which classes to run? By default name patterns: Test*, *Test, *Tests and *TestCase; other classes need to be added through includes.

Конфігурація maven-surefire-plugin у pom.xml для запуску TestNG
Configuring maven-surefire-plugin in pom.xml for TestNG execution
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.2.5</version>
            <configuration>
                <suiteXmlFiles>
                    <suiteXmlFile>src/test/resources/testng-smoke.xml</suiteXmlFile>
                </suiteXmlFiles>
                <parallel>methods</parallel>
                <threadCount>4</threadCount>
            </configuration>
        </plugin>
    </plugins>
</build>
💡 Простими словами: Maven-surefire-plugin — це вбудований інструмент збірки, який автоматично знаходить і запускає ваші модульні тести (JUnit або TestNG) під час виконання команди mvn test. Він запускає їх в ізольованому середовищі та зберігає результати перевірок у вигляді звітів, які потім аналізує система CI/CD.
💡 In plain words: The maven-surefire-plugin is a built-in build tool that automatically discovers and executes your unit tests (JUnit or TestNG) when you run the mvn test command. It runs them in an isolated environment and saves the test outcomes as reports for CI/CD analysis.
🗣 Як читати вголос За запуск тестів у Maven відповідає плагін maven-surefire-plugin. Він автоматично активується на фазі test і сканує проєкт на наявність тестових класів, які відповідають визначеним правилам іменування. Під капотом цей плагін форкає окрему віртуальну машину Java, що забезпечує ізоляцію середовища виконання тестів від решти процесу збірки. Я часто конфігурую його в pom-файлі, якщо потрібно передати XML-файли сюїт TestNG або увімкнути паралелізацію методів для прискорення перевірок. Головне — не плутати його з плагіном failsafe, який створений спеціально для інтеграційних тестів і виконується на пізніших фазах життєвого циклу Maven.
🗣 How to say it out loud Test execution in Maven is handled by the maven-surefire-plugin. It automatically activates during the test phase and scans the project for test classes matching specific naming conventions. Under the hood, this plugin forks a separate JVM process, which ensures the test execution environment remains isolated from the rest of the build process. I frequently configure it in the pom file whenever I need to pass TestNG suite XML files or enable method-level parallelization to speed up test runs. The key thing is not to confuse it with the failsafe plugin, which is built specifically for integration tests and runs during later phases of the Maven lifecycle.
31 Який Maven плагін запускає integration-тести? Which Maven plugin runs integration tests?
Збірка, залежності та Git Build, dependencies and Git mid ▶

Що це — Для запуску інтеграційних тестів у проєктах на Maven використовується плагін Maven Failsafe Plugin (maven-failsafe-plugin). На відміну від maven-surefire-plugin, який відповідає виключно за юніт-тести, Failsafe розроблений спеціально для життєвого циклу інтеграційного тестування, де часто потрібні зовнішні ресурси, такі як реляційні бази даних, тестові контейнери чи підняті вебсервери.

What is it — To run integration tests in Maven projects, the Maven Failsafe Plugin (maven-failsafe-plugin) is used. Unlike the maven-surefire-plugin, which is exclusively responsible for unit tests, Failsafe is specifically designed for the integration testing lifecycle where external resources such as relational databases, test containers, or running web servers are often required.

Навіщо/коли — Головна технічна особливість Failsafe полягає в тому, як він взаємодіє з фазами життєвого циклу збірки Maven. Він розбиває процес на три послідовні етапи: pre-integration-test (підготовка оточення, наприклад, запуск Docker-контейнерів через Testcontainers), integration-test (безпосереднє виконання тестів із суфіксом *IT.java) та post-integration-test (очищення ресурсів). Завдяки такій архітектурі та фазі verify, навіть у разі падіння самих тестів етап очищення середовища гарантовано виконується, що запобігає зависанню процесів та займанню портів у пайплайнах CI/CD.

Why/when — The key technical feature of Failsafe is how it interacts with the Maven build lifecycle phases. It splits the process into three sequential stages: pre-integration-test (environment preparation, such as starting Docker containers via Testcontainers), integration-test (actual execution of tests with the *IT.java suffix), and post-integration-test (resource cleanup). Thanks to this architecture and the verify phase, even if the tests themselves fail, the environment cleanup stage is guaranteed to run, preventing hanging processes and occupied ports in CI/CD pipelines.

Приклад з проєкту — У реальному проєкті автоматизації на Java та Spring Boot ми зазвичай конфігуруємо цей плагін у файлі pom.xml для запуску тестів, що перевіряють інтеграцію репозиторіїв із реальним PostgreSQL. Виконання команди mvn verify запускає спочатку юніт-тести на етапі test (через Surefire), потім піднімає базу даних, виконує інтеграційні тести за допомогою Failsafe, а після цього обов'язково гасить контейнери незалежно від результатів перевірки.

Project example — In a real Java and Spring Boot automation project, we typically configure this plugin in the pom.xml file to run tests that verify repository integration with a real PostgreSQL. Running the mvn verify command first executes unit tests during the test phase (via Surefire), then spins up the database, runs integration tests using Failsafe, and finally guarantees container shutdown regardless of the test outcomes.

Підводні камені — Поширена помилка новачків — спроба запускати інтеграційні тести через maven-surefire-plugin, називаючи їх класи за стандартним патерном *Test.java, або ж запуск лише цілі integration-test без подальшої фази verify. Це призводить до того, що тести або запускаються не в той момент часу і сповільнюють звичайну збірку, або залишають незакриті підключення до БД та завислі процеси, оскільки Surefire завершує збірку помилкою та пропускає етап очищення середовища.

Pitfalls — A common beginner mistake is trying to run integration tests using the maven-surefire-plugin by naming their classes with the standard *Test.java pattern, or running only the integration-test goal without the subsequent verify phase. This causes tests to either run at the wrong time and slow down standard builds, or leave open DB connections and hanging processes because Surefire fails the build and skips the environment cleanup stage.

Уточнення на співбесіді — Чому Failsafe не падає одразу на фазі integration-test? Щоб фаза post-integration-test встигла зупинити середовище; результат перевіряє ціль verify.

Interview follow-up — Why doesn't Failsafe fail right at the integration-test phase? So that post-integration-test can stop the environment; the verify goal checks the result.

Приклад конфігурації maven-failsafe-plugin у pom.xml
Example of maven-failsafe-plugin configuration in pom.xml
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>3.2.5</version>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
        </execution>
    </executions>
</plugin>
💡 Простими словами: Для юніт-тестів використовується плагін Surefire (фаза test), а для інтеграційних тестів, яким потрібні зовнішні сервери чи бази даних — Failsafe (фаза verify). Головна перевага Failsafe полягає в тому, що він гарантовано очистить середовище та зупинить контейнери навіть у разі падіння самих тестів.
💡 In plain words: The Surefire plugin is used for unit tests (the test phase), while Failsafe is used for integration tests that require external servers or databases (the verify phase). The key advantage of Failsafe is that it guarantees cleanup of the environment and stops containers even if the tests themselves fail.
🗣 Як читати вголос Для запуску інтеграційних тестів у Maven ми використовуємо плагін maven-failsafe-plugin. Він принципово відрізняється від surefire тим, що створений спеціально для роботи із зовнішніми ресурсами та забезпечує безпечний життєвий цикл з обов'язковим очищенням середовища. Failsafe міцно прив'язаний до фази verify, тому навіть якщо якісь інтеграційні тести впадуть через баги, плагін спочатку виконає пост-тестові процедури та зупинить тестові контейнери чи сервери. Зазвичай ми налаштовуємо його на пошук класів із суфіксом IT, що дозволяє чітко розмежувати швидкі юніти та важкі інтеграційні тести у наших CI/CD пайплайнах.
🗣 How to say it out loud To run integration tests in Maven, we use the maven-failsafe-plugin. It fundamentally differs from Surefire because it is built specifically to work with external resources and provides a safe lifecycle with mandatory environment cleanup. Failsafe is tightly bound to the verify phase, so even if some integration tests fail due to bugs, the plugin will first execute post-test procedures and shut down test containers or servers. Usually, we configure it to look for classes with the IT suffix, which allows us to clearly separate fast unit tests and heavy integration tests in our CI/CD pipelines.
32 Як пропустити тести при збірці Maven? How do you skip tests during a Maven build?
Збірка, залежності та Git Build, dependencies and Git mid ▶

Що це — Щоб пропустити виконання тестів під час збірки проєкту за допомогою Maven, найчастіше використовують прапорець командного рядка -DskipTests або його альтернативу -Dmaven.test.skip=true. Основна різниця між ними полягає в тому, що -DskipTests компілює тестові класи, але пропускає їх безпосереднє виконання (фазу test), тоді як -Dmaven.test.skip=true повністю виключає і компіляцію, і виконання тестів.

What is it — To skip test execution during a Maven build, the most common approach is using the command-line flag -DskipTests or its alternative -Dmaven.test.skip=true. The main difference between them is that -DskipTests compiles test classes but skips their actual execution (the test phase), whereas -Dmaven.test.skip=true completely bypasses both compilation and execution of tests.

Навіщо/коли — Цей механізм працює на рівні плагіна збірки Maven Surefire Plugin (для юніт-тестів) та Maven Failsafe Plugin (для інтеграційних тестів). Передаючи цей параметр, ми змінюємо конфігурацію плагіна «на льоту», наказуючи йому проігнорувати стандартні життєві цикли тестування. Maven продовжує збирати основний код та пакувати його у JAR/WAR-артефакт, суттєво прискорюючи процес збірки.

Why/when — This mechanism works at the level of the Maven Surefire Plugin (for unit tests) and Maven Failsafe Plugin (for integration tests). By passing this parameter, we override the plugin configuration on the fly, instructing it to ignore the standard testing life cycles. Maven continues to build the main code and package it into a JAR/WAR artifact, significantly speeding up the build process.

Приклад з проєкту — Цей підхід вкрай корисний під час локальної розробки, коли розробник чи QA-інженер робить швидкі зміни у конфігурації, перевіряє збірку або хоче перевірити працездатність артефакту без очікування довготривалого юніт-тест-сьюту. Водночас це категорично заборонено використовувати у CI/CD пайплайнах для збирання артефактів, що йдуть на розгортання в середовища тестування чи продакшну.

Project example — This approach is extremely useful during local development when a developer or QA engineer makes quick configuration changes, verifies the build, or wants to check artifact functionality without waiting for a long unit test suite. At the same time, it is strictly forbidden to use this in CI/CD pipelines for building artifacts destined for deployment to test or production environments.

Підводні камені — Типовим сценарієм на проєкті є ситуація, коли локальний набір з 500+ тестів виконується понад 10 хвилин. QA-автоматизатор змінює невеликий фрагмент коду інфраструктури та хоче перевірити, чи збирається проєкт без помилок компіляції. Замість того щоби щоразу чекати виконання всіх тестів, він запускає швидку збірку з параметром пропуску, а повну перевірку залишає для нічного або гілкового пайплайну.

Pitfalls — A typical project scenario occurs when a local suite of 500+ tests takes over 10 minutes to execute. A QA automation engineer modifies a small piece of infrastructure code and wants to verify if the project compiles without errors. Instead of waiting for all tests to run every time, they trigger a quick build with the skip parameter, leaving full verification for the nightly or branch pipeline.

Уточнююче питання — Поширеною помилкою є використання лише -DskipTests у проєктах, де активно використовуються інтеграційні тести через Maven Failsafe Plugin. Оскільки Surefire та Failsafe — це різні плагіни, стандартний прапорець не пропустить інтеграційні тести (фазу integration-test). Для них потрібно додатково вказувати -DskipITs=true або використовувати загальний -Dmaven.test.skip=true.

Follow-up question — A common mistake is relying solely on -DskipTests in projects that heavily use integration tests via the Maven Failsafe Plugin. Since Surefire and Failsafe are separate plugins, the standard flag will not skip integration tests (the integration-test phase). For those, you must additionally specify -DskipITs=true or use the global -Dmaven.test.skip=true.

Приклад використання командного рядка та конфігурації pom.xml
Example of command line usage and pom.xml configuration
# Швидка збірка проєкту з пропуском юніт-тестів у командному рядку
mvn clean package -DskipTests

# Пропуск як юніт, так і інтеграційних тестів
mvn clean install -DskipTests -DskipITs

# Або фіксація пропуску безпосередньо у pom.xml в секції plugins
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.0.0</version>
    <configuration>
        <skipTests>true</skipTests>
    </configuration>
</plugin>
# Quick build that skips unit tests from the command line
mvn clean package -DskipTests

# Skip both unit and integration tests
mvn clean install -DskipTests -DskipITs

# Or set the skip directly in pom.xml under plugins
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.0.0</version>
    <configuration>
        <skipTests>true</skipTests>
    </configuration>
</plugin>
💡 Простими словами: Пропуск тестів у Maven робиться за допомогою параметра -DskipTests у командному рядку або конфігурації Surefire плагіна. Це економить час під час локальної розробки, але ніколи не має замінювати запуск тестів у CI/CD пайплайнах перед злиттям коду.
💡 In plain words: Skipping tests in Maven is done using the -DskipTests parameter in the command line or via the Surefire plugin configuration. This saves time during local development, but should never replace running tests in CI/CD pipelines before merging code.
🗣 Як читати вголосЧасто під час локальної розробки чи гарячих фіксів мені потрібно швидко зібрати проєкт, не чекаючи завершення всіх юніт-тестів. Для цього я використовую прапорець -DskipTests у команді Maven, наприклад mvn clean package -DskipTests. Важливо розуміти різницю між skipTests та maven.test.skip: перший компілює тестові класи, але не запускає їх, тоді як другий повністю пропускає і компіляцію, і виконання. Також варто пам'ятати, що якщо в проєкті є інтеграційні тести через Failsafe plugin, для їх пропуску потрібен окремий прапорець -DskipITs. Я ніколи не використовую цей трюк у CI/CD для зборки артефактів, які йдуть на тестування чи прод, щоб випадково не задеплоїти зламаний білд.
🗣 How to say it out loudOften during local development or hotfixes, I need to quickly build the project without waiting for all unit tests to finish. For this, I use the -DskipTests flag in the Maven command, like mvn clean package -DskipTests. It's important to understand the difference between skipTests and maven.test.skip: the former compiles test classes but doesn't run them, whereas the latter completely bypasses both compilation and execution. I also keep in mind that if the project has integration tests managed by the Failsafe plugin, I need a separate -DskipITs flag to skip them. I never use this shortcut in CI/CD for artifacts going to testing or production environments, to avoid accidentally deploying a broken build.
33 Що таке pom.xml? What is pom.xml?
Збірка, залежності та Git Build, dependencies and Git junior ▶

Що це — pom.xml (Project Object Model) — це головний файл конфігурації та «паспорт» проєкту в системі управління збиранням Maven. Він описує всю структуру проєкту, його унікальні координати, зовнішні залежності, плагіни та життєвий цикл збирання у форматі XML.

What is it — pom.xml (Project Object Model) is the core configuration file and "passport" of a project in the Maven build management system. It describes the entire project structure, its unique coordinates, external dependencies, plugins, and the build lifecycle in XML format.

Навіщо/коли — Цей файл працює за принципом декларативного опису: ви вказуєте «що» саме потрібно вашому проєкту (наприклад, певна версія Selenium чи TestNG), а Maven самостійно вирішує «як» це отримати. Він автоматично завантажує необхідні бібліотеки з центрального репозиторію та керує транзитивними залежностями — тобто підтягує всі супутні модулі, від яких залежить основна бібліотека.

Why/when — This file works on the principle of declarative description: you specify "what" your project needs (e.g., a specific version of Selenium or TestNG), and Maven figures out "how" to get it. It automatically downloads the required libraries from the central repository and manages transitive dependencies—pulling in all companion modules that the main library relies on.

Приклад з проєкту — У реальній практиці QA-автоматизатора pom.xml є основою для побудови тест-фреймворку. Саме сюди додаються бібліотеки для роботи з браузерами, API (RestAssured), звітності (Allure) та управління тестовим середовищем. Крім того, через спеціальні плагіни (наприклад, Maven Surefire Plugin) у pom-файлі налаштовується запуск тестів під час збирання проєкту або інтеграції в CI/CD пайплайни на зразок Jenkins чи GitHub Actions.

Project example — In a QA automation engineer's real practice, pom.xml is the foundation for building a test framework. This is where libraries for browser interaction, API testing (RestAssured), reporting (Allure), and environment management are added. Furthermore, through special plugins (such as the Maven Surefire Plugin), the pom file configures test execution during the build process or integration into CI/CD pipelines like Jenkins or GitHub Actions.

Підводні камені — Найпоширенішими помилками при роботі з pom.xml є жорстке хардкодження версій бібліотек без використання тегу <properties>, що веде до конфліктів версій, а також накопичення зайвих (невикористаних) залежностей. Також критично важливо слідкувати за синтаксисом XML, адже навіть одна пропущена закриваюча тег-дужка призведе до повної зупинки збирання проєкту.

Pitfalls — The most common mistakes when working with pom.xml include hardcoding library versions without using the <properties> tag, which leads to version conflicts, as well as accumulating redundant (unused) dependencies. It is also critically important to monitor XML syntax, because even a single missed closing tag will cause the project build to fail completely.

Уточнення на співбесіді — Чим dependencyManagement відрізняється від dependencies? Перший лише фіксує версії для модулів, а другий реально підключає бібліотеки до збірки.

Interview follow-up — How does dependencyManagement differ from dependencies? The first only pins versions for modules, the second actually adds the libraries to the build.

Приклад базового файлу pom.xml для проєкту автоматизації
Example of a basic pom.xml file for an automation project
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.qa.automation</groupId>
    <artifactId>ui-test-framework</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <selenium.version>4.18.0</selenium.version>
        <testng.version>7.9.0</testng.version>
    </properties>

    <dependencies>
        <!-- Selenium WebDriver -->
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>${selenium.version}</version>
        </dependency>

        <!-- TestNG Framework -->
        <dependency>
            <groupId>org.testng</groupId>
            <artifactId>testng</artifactId>
            <version>${testng.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
💡 Простими словами: pom.xml — це головний «список покупок» та інструкція для вашого Java-проєкту. У ньому ви просто прописуєте назви та версії потрібних бібліотек (наприклад, Selenium), а Maven сам їх знаходить, завантажує і налаштовує, позбавляючи вас потреби вручну шукати та закидати файли в проєкт.
💡 In plain words: pom.xml is the main "shopping list" and instruction manual for your Java project. You simply declare the names and versions of the libraries you need (like Selenium), and Maven automatically finds, downloads, and configures them, saving you from manually hunting for files and dropping them into the project.
🗣 Як читати вголос Це файл конфігурації у форматі XML для системи Maven, який фактично є серцем будь-якого Java-проєкту. У ньому я задаю всю основну інформацію про проєкт: групу, назву, версію, а також керую всіма зовнішніми залежностями. Наприклад, коли я пишу фреймворк для автоматизації, я додаю сюди бібліотеки Selenium, TestNG чи RestAssured, просто вказавши їхні координати. Maven сам завантажує їх із центрального репозиторію, що страшенно зручно, бо не потрібно вручну завантажувати й переносити JAR-файли. Також у pom.xml налаштовуються плагіни збирання та виконання тестів, які потім успішно запускаються в CI/CD пайплайнах.
🗣 How to say it out loud It is an XML configuration file for Maven, which acts as the core heart of any Java project. Inside it, I define all the main project metadata like group, artifact ID, version, and manage all external dependencies. For instance, when building an automation framework, I add Selenium, TestNG, or RestAssured libraries here by simply specifying their coordinates. Maven downloads them automatically from the central repository, which is extremely convenient because I don't have to manually download and manage JAR files. Additionally, pom.xml is used to configure build and test plugins that later run seamlessly inside CI/CD pipelines.
34 Яка команда Gradle запускає тести? Which Gradle command runs tests?
Збірка, залежності та Git Build, dependencies and Git junior ▶

Що це — Основною командою для запуску тестів у проєкті, що використовує систему збірки Gradle, є gradle test, проте на практиці завжди використовується її безпечний аналог через обгортку — ./gradlew test. Ця команда ініціює процес компіляції як основного коду програми, так і коду тестів, після чого передає керування тестовому фреймворку (наприклад, JUnit або TestNG) для виконання сценаріїв.

What is it — The primary command for running tests in a project utilizing the Gradle build system is gradle test, though in practice, its secure wrapper equivalent ./gradlew test is always used. This command initiates the compilation process for both the main application code and test code, and then hands over execution control to the testing framework (such as JUnit or TestNG) to run the scenarios.

Навіщо/коли — Під капотом Gradle спирається на концепцію тасків (tasks) та граф залежностей. Коли ви викликаєте таск test, Gradle спочатку автоматично перевіряє та виконує попередні залежні задачі — такі як compileJava та compileTestJava. Після завершення тестів Gradle формує детальні звіти у форматі XML та HTML, які автоматично зберігаються в директорії проєкту build/reports/tests/test/index.html для зручного аналізу результатів.

Why/when — Under the hood, Gradle relies on the concept of tasks and a dependency graph. When you invoke the test task, Gradle automatically checks and executes preceding dependent tasks first—such as compileJava and compileTestJava. Once the tests finish, Gradle generates detailed reports in XML and HTML formats, which are automatically saved in the project directory at build/reports/tests/test/index.html for convenient result analysis.

Приклад з проєкту — Автоматизатори часто використовують розширені варіації цієї команди залежно від поточних завдань. Наприклад, під час локальної розробки та дебагу недоцільно запускати весь набір тестів; для цього застосовують фільтрацію за допомогою прапорця --tests, наприклад: ./gradlew test --tests "AuthTest.loginSuccess". Натомість повний запуск команди ./gradlew test зазвичай виконується на етапі регресійного тестування, перед створенням Pull Request або безпосередньо в CI/CD пайплайнах (наприклад, у Jenkins чи GitHub Actions).

Project example — Automation engineers frequently use extended variations of this command depending on their current tasks. For example, during local development and debugging, running the entire test suite is inefficient; instead, filtering is applied using the --tests flag, such as ./gradlew test --tests "AuthTest.loginSuccess". Conversely, a full execution of ./gradlew test is typically run during regression testing, before creating a Pull Request, or directly within CI/CD pipelines (such as Jenkins or GitHub Actions).

Підводні камені — Типовою помилкою серед початківців є запуск глобальної команди gradle test замість використання локального Gradle Wrapper (./gradlew), що призводить до конфліктів версій Gradle на комп'ютері розробника та на білд-сервері. Інша поширена помилка — очікування, що стандартний таск test за замовчуванням виконуватиме UI або інтеграційні тести, хоча у багатьох конфігураціях вони винесені в окремі кастомні source sets та вимагають виклику інших тасків (наприклад, ./gradlew integrationTest).

Pitfalls — A common mistake among juniors is running the global gradle test command instead of using the local Gradle Wrapper (./gradlew), which leads to Gradle version conflicts between the developer's machine and the build server. Another widespread mistake is assuming that the standard test task will execute UI or integration tests by default, whereas in many configurations, these are moved to separate custom source sets and require invoking other tasks (e.g., ./gradlew integrationTest).

Уточнення на співбесіді — Як запустити один тестовий клас у Gradle? Через фільтр --tests, наприклад ./gradlew test --tests LoginTest, і так само за шаблоном пакета.

Interview follow-up — How do you run a single test class in Gradle? With the --tests filter, e.g. ./gradlew test --tests LoginTest, and the same works with a package pattern.

Приклад виконання команди через Gradle Wrapper у терміналі
Example of running the command via Gradle Wrapper in the terminal
$ ./gradlew test

> Task :compileJava UP-TO-DATE
> Task :compileTestJava
> Task :test

BUILD SUCCESSFUL in 4s
3 actionable tasks: 3 executed
💡 Простими словами: Головна команда для запуску тестів у Gradle — це ./gradlew test. Завжди використовуйте її через обгортку (з крапкою і слешем на початку), щоб гарантувати запуск правильної версії Gradle, зашитої в проєкт.
💡 In plain words: The main command to run tests in Gradle is ./gradlew test. Always use it via the wrapper (with a dot-slash prefix) to guarantee the execution of the exact Gradle version configured for the project.
🗣 Як читати вголосДля запуску тестів у проєкті з Gradle ми використовуємо команду ./gradlew test. Я завжди наголошую на важливості використання саме Gradle Wrapper, тобто з крапкою і слешем на початку, щоб уникнути розсинхронізації версій на локальній машині та в CI/CD. Ця команда послідовно компілює код, виконує юніт-тести та генерує детальний HTML-звіт у папці build. Якщо мені потрібно перевірити лише один конкретний тест під час налагодження, я додаю фільтр --tests. Варто також пам'ятати, що стандартний таск test зазвичай відповідає за юніт-тести, тоді як інтеграційні чи UI-тести найчастіше вимагають окремих кастомних тасків.
🗣 How to say it out loudTo run tests in a Gradle project, we use the ./gradlew test command. I always emphasize using the Gradle Wrapper with a dot and slash prefix to prevent version mismatches between the local machine and the CI/CD pipeline. This command sequentially compiles the code, executes unit tests, and generates a detailed HTML report inside the build folder. If I need to debug a single specific test, I append the --tests filter. It's also worth keeping in mind that the standard test task usually handles unit tests, while integration or UI tests most often require separate custom tasks.
35 Що таке Gradle wrapper (gradlew)? What is the Gradle wrapper (gradlew)?
Збірка, залежності та Git Build, dependencies and Git mid ▶

Що це — Gradle wrapper (gradlew для Linux/macOS та gradlew.bat для Windows) — це спеціальний набір скриптів і файлів конфігурації, який постачається разом із проєктом та дозволяє запускати збірку й тести без попереднього встановлення Gradle у самій операційній системі.

What is it — The Gradle wrapper (gradlew for Linux/macOS and gradlew.bat for Windows) is a specific set of scripts and configuration files bundled with the project that allows you to run builds and tests without having Gradle pre-installed on the operating system.

Навіщо/коли — Механізм обгортки базується на файлі gradle-wrapper.properties, де чітко вказана конкретна версія Gradle, необхідна для цього проєкту. Коли ви вперше запускаєте команду gradlew, спеціальний файл gradle-wrapper.jar автоматично перевіряє наявність потрібної версії в системі та, за потреби, завантажує її з офіційного репозиторію в локальний кеш розробника чи CI-агента.

Why/when — The wrapper mechanism relies on the gradle-wrapper.properties file, which explicitly defines the specific Gradle version required for the project. When you first execute a gradlew command, the gradle-wrapper.jar automatically checks if the required version is present locally and, if not, downloads it from the official repository into the developer's or CI agent's local cache.

Приклад з проєкту — Для QA-інженерів автоматизації Gradle wrapper є критично важливим елементом інфраструктури, оскільки він гарантує повну відтворюваність середовища (reproducibility). Завдяки йому і локальні комп'ютери команди, і сервери неперервної інтеграції (Jenkins, GitLab CI, GitHub Actions) використовують абсолютно ідентичну версію білд-системи, що повністю усуває класичну проблему «у мене все працює локально, а на пайплайні падає через невідповідність версій».

Project example — For QA automation engineers, the Gradle wrapper is a critical infrastructure element because it guarantees full environment reproducibility. Thanks to it, both the team's local machines and continuous integration servers (Jenkins, GitLab CI, GitHub Actions) use the exact same version of the build system, completely eliminating the classic "it works locally on my machine, but fails in the pipeline due to version mismatch" issue.

Підводні камені — Типовою помилкою під час роботи з Gradle wrapper є спроба завантажити та закомітити в Git увесь важкий кеш дистрибутивів (наприклад, папку dists), замість того щоб зберігати виключно легкі скрипти та конфігураційні файли. Крім того, на Unix-системах поширеною проблемою є втрата прав на виконання для файлу gradlew, через що перед запуском тестів у CI обов'язково застосовують команду chmod +x gradlew.

Pitfalls — A common mistake when working with the Gradle wrapper is attempting to commit the heavy directory of downloaded distributions (such as the dists folder) into Git, instead of tracking exclusively the lightweight scripts and configuration files. Additionally, on Unix-based systems, a frequent issue is the loss of execution permissions for the gradlew file, which is why the chmod +x gradlew command must be applied before running tests in CI.

Уточнення на співбесіді — Чи треба комітити файли wrapper у репозиторій? Так: gradlew, gradlew.bat і каталог gradle/wrapper мають бути в Git, інакше CI не зможе відтворити версію Gradle.

Interview follow-up — Should the wrapper files be committed? Yes: gradlew, gradlew.bat and the gradle/wrapper directory belong in Git, otherwise CI cannot reproduce the Gradle version.

Приклад локального запуску тестів та використання Gradle wrapper у CI/CD пайплайні
Example of running tests locally and using the Gradle wrapper in a CI/CD pipeline
# Локальний запуск регресійного набору тестів через обгортку Gradle
./gradlew clean test -Denv=qa -Dbrowser=chrome

# Фрагмент конфігурації кроку в GitHub Actions / GitLab CI
- name: Run UI Automation Tests
  run: |
    chmod +x gradlew
    ./gradlew runRegressionSuite --info
# Run the regression suite locally through the Gradle wrapper
./gradlew clean test -Denv=qa -Dbrowser=chrome

# A step config fragment for GitHub Actions / GitLab CI
- name: Run UI Automation Tests
  run: |
    chmod +x gradlew
    ./gradlew runRegressionSuite --info
💡 Простими словами: Gradle wrapper — це такий собі автономний менеджер збірки всередині самого проєкту. Завдяки йому вам не потрібно встановлювати Gradle вручну на кожен новий комп'ютер чи CI/CD-сервер — достатньо запустити скрипт gradlew, який самостійно завантажить і застосує саме ту версію інструменту, яка закладена розробниками.
💡 In plain words: The Gradle wrapper is essentially a self-contained build manager embedded right inside the project. Thanks to it, you don't need to manually install Gradle on every new machine or CI/CD server — simply running the gradlew script will automatically download and apply the exact version of the tool intended by the developers.
🗣 Як читати вголосGradle wrapper або gradlew — це спеціальний набір скриптів і файлу конфігурації, який дозволяє запускати збірку та автоматизовані тести на будь-якій машині без необхідності встановлювати Gradle глобально в операційній системі. У моїй щоденній роботі з автоматизацією це незамінно, оскільки я запускаю тести як на своєму ноутбуці, так і на CI-сервері однією й тією ж командою ./gradlew test, маючи 100% гарантію однакової поведінки середовища. Сам файл gradle-wrapper.properties чітко фіксує версію білд-системи, а легкий gradle-wrapper.jar за потреби автоматично завантажує її з інтернету. Це повністю позбавляє команду проблем із несумісністю версій та частих конфліктів у командній розробці.
🗣 How to say it out loudThe Gradle wrapper, or gradlew, is a dedicated set of scripts and a configuration file that lets you run builds and automated tests on any machine without needing to install Gradle globally on the operating system. In my daily automation work, this is indispensable because I run tests both on my laptop and on the CI server using the exact same ./gradlew test command, giving me a 100% guarantee of consistent environment behavior. The gradle-wrapper.properties file explicitly locks down the build system version, and the lightweight gradle-wrapper.jar automatically downloads it from the internet when needed. This completely eliminates version incompatibility issues and frequent build discrepancies across team members' environments.
36 Яка команда Git створює нову гілку і переходить на неї? Which Git command creates a new branch and switches to it?
Збірка, залежності та Git Build, dependencies and Git junior ▶

Що це — Основною командою для створення нової гілки та одночасного переходу на неї є git checkout -b <branch_name> або її сучасніший і безпечніший аналог git switch -c <branch_name>. Ця команда є комбінацією двох базових операцій: створення нового вказівника гілки від поточного стану репозиторію та миттєвого перемикання робочого середовища на неї.

What is it — The primary command to create a new branch and switch to it simultaneously is git checkout -b <branch_name> or its more modern and safer alternative git switch -c <branch_name>. This command combines two basic operations: creating a new branch pointer from the current repository state and instantly switching the working environment to it.

Навіщо/коли — Механізм цієї команди працює «під капотом» так само, як послідовне виконання двох окремих команд: спочатку створюється гілка за допомогою git branch <branch_name>, а потім активується через git checkout <branch_name> або git switch <branch_name>. Використання єдиного прапорця -b чи -c суттєво економить час розробників та тестувальників, запобігаючи зайвим ручним крокам у щоденній роботі з контролем версій.

Why/when — Under the hood, the mechanism of this command works the same as sequentially executing two separate commands: first creating the branch using git branch <branch_name>, and then activating it via git checkout <branch_name> or git switch <branch_name>. Using a single flag like -b or -c significantly saves time for developers and testers by preventing unnecessary manual steps in daily version control work.

Приклад з проєкту — У повсякденній роботі QA-інженера цю команду використовують для локального тестування нових фіч, перевірки виправлених багів перед злиттям у загальний репозиторій, або коли потрібно створити ізольоване середовище для розробки та запуску автотестів. Вона незамінна для проведення регресійного тестування окремих гілок або виконання локального аналізу проблеми без ризику зіпсувати стабільний код.

Project example — In the daily routine of a QA engineer, this command is used for local feature testing, verifying bug fixes before merging into the main repository, or when an isolated environment is needed for developing and running automated tests. It is indispensable for performing regression testing on specific branches or running local troubleshooting without risking damage to stable code.

Підводні камені — Уявіть типовий проєктний сценарій: розробник надіслав виправлення критичного бага для задачі у гілку develop, і вам потрібно перевірити цей код на своїй машині. Ви оновлюєте локальний develop, після чого створюєте робочу гілку командою git checkout -b qa/bugfix-PROJ-456. Це дозволяє зосередитися на перевірці конкретного завдання в ізольованому просторі, зберігаючи чистим основний робочий простір.

Pitfalls — Imagine a typical project scenario: a developer has submitted a fix for a critical bug to the develop branch, and you need to verify this code on your machine. You update your local develop, and then create a working branch with the command git checkout -b qa/bugfix-PROJ-456. This allows you to focus on verifying the specific task in an isolated space while keeping your main workspace clean.

Уточнююче питання — Серед поширених помилок новачків варто виділити створення гілки від застарілого стану репозиторію через відсутність попереднього оновлення (забули виконати git pull), а також спробу створити нову гілку при наявності незакомічених локальних змін. Останнє часто призводить до перенесення цих незбережених файлів у нову гілку або конфліктів. Крім того, варто пам'ятати, що команда git switch є сучаснішою і менш перевантаженою додатковим функціоналом порівняно зі старою командою checkout.

Follow-up question — Among common beginner mistakes are creating a branch from an outdated repository state due to missing a prior update (forgetting to run git pull), and attempting to create a new branch while having uncommitted local changes. The latter often leads to carrying those unsaved files over to the new branch or causing conflicts. Additionally, it is worth remembering that the git switch command is more modern and less overloaded with extra functionality compared to the legacy checkout command.

Приклад створення нової робочої гілки для QA
Example of creating a new working branch for QA
# 1. Переходимо на актуальну базову гілку та оновлюємо її
git checkout develop
git pull origin develop

# 2. Створюємо нову гілку для тестування задачі та одразу переходимо на неї
git checkout -b qa/feature-payment-validation

# Або за допомогою сучасного синтаксису switch:
git switch -c qa/feature-payment-validation
# 1. Switch to the up-to-date base branch and pull it
git checkout develop
git pull origin develop

# 2. Create a new branch for the task and switch to it at once
git checkout -b qa/feature-payment-validation

# Or with the newer switch syntax:
git switch -c qa/feature-payment-validation
💡 Простими словами: Ця команда працює як створення особистої копії робочого зошита команди, куди ви можете вносити власні позначки чи перевіряти код, не чіпаючи при цьому основні записи інших учасників проєкту.
💡 In plain words: This command works like creating your own copy of the team's workbook where you can make your own notes or test code without touching the main records of other project participants.
🗣 Як читати вголос Коли мені потрібно розпочати перевірку нової фічі чи баг-фіксу, я зазвичай використовую команду git checkout -b або її сучасніший аналог git switch -c. Вона дуже зручна, тому що об'єднує дві дії в одну: створення гілки та миттєвий перехід на неї. Наприклад, перед цим я обов'язково оновлюю актуальний стан репозиторію через git pull, а потім створюю свою гілку на кшталт qa/task-number. Головне — робити це за чистого робочого стану, щоб мої локальні незбережені зміни випадково не перейшли на нову гілку.
🗣 How to say it out loud When I need to start verifying a new feature or bug fix, I typically use the git checkout -b command or its more modern alternative git switch -c. It is very convenient because it combines two actions into one: creating the branch and switching to it instantly. For example, before doing this, I always update the current state of the repository via git pull, and then create my branch like qa/task-number. The main thing is to do this from a clean working state so my local uncommitted changes don't accidentally carry over to the new branch.
37 Яка різниця між git merge та git rebase? What is the difference between git merge and git rebase?
Збірка, залежності та Git Build, dependencies and Git mid ▶

Що це — Основна відмінність між git merge та git rebase полягає в тому, як вони керують та об'єднують історію коміктів. Обидві команди слугують одній меті — інтеграції змін з однієї гілки в іншу, проте роблять це принципово різними методами, що безпосередньо впливає на читабельність історії проєкту та командну роботу.

What is it — The core difference between git merge and git rebase lies in how they manage and combine commit history. Both commands serve the same purpose—integrating changes from one branch into another—yet they do so using fundamentally different methods, which directly affects project history readability and team collaboration.

Навіщо/коли — Механізм роботи merge: Команда git merge бере дві гілки та їхній спільний попередній комікт (common ancestor), після чого створює новий спільний «комікт злиття» (merge commit). Цей новий комікт має два батьківські елементи та повністю зберігає всю хронологію розгалуження та паралельної розробки у вигляді незмінного дерева.

Why/when — Merge mechanism: The git merge command takes two branches and their common ancestor, then creates a new «merge commit». This new commit has two parent elements and fully preserves the entire timeline of branching and parallel development as an immutable tree.

Приклад з проєкту — Механізм роботи rebase: Команда git rebase діє інакше: вона тимчасово знімає ваші локальні комікти з поточної гілки, перемикається на верхівку цільової гілки (наприклад, main) і послідовно «накочує» (replays) ваші комікти один за одним зверху. При цьому створюються нові комікти з новими хешами, а історія стає повністю лінійною, ніби ви писали код одразу на актуальній версії головної гілки.

Project example — Rebase mechanism: The git rebase command works differently: it temporarily removes your local commits from the current branch, switches to the tip of the target branch (e.g., main), and sequentially replays your commits one by one on top. This generates new commits with new hashes, making the history completely linear as if you wrote code directly on the latest version of the main branch.

Підводні камені — Коли що використовувати в реальних проєктах: git merge є безпечним стандартом для спільних або публічних гілок (наприклад, main, develop), оскільки він не змінює минуле і не ламає синхронізацію у колег. git rebase найкраще використовувати локально на власних фіча-гілках перед створенням Pull Request, щоб прибрати зайві проміжні комікти на кшталт «fix typo» чи «wip» і подарувати код-рев'юерам чисту лінійну історію.

Pitfalls — When to use what in real projects: git merge is a safe standard for shared or public branches (like main, develop) because it does not alter the past and doesn't break synchronization for teammates. git rebase is best used locally on your own feature branches before creating a Pull Request to remove messy intermediate commits like «fix typo» or «wip» and provide code reviewers with a clean linear history.

Уточнююче питання — Типові помилки: Головна небезпека — використання git rebase на спільних гілках з подальшим примусовим пушем (git push --force). Це повністю перезаписує історію на віддаленому сервері, через що інші члени команди стикаються з нерозв'язними конфліктами синхронізації та втратою робочого контексту.

Follow-up question — Common mistakes: The primary danger is using git rebase on shared branches followed by a force push (git push --force). This completely rewrites history on the remote server, causing other team members to face intractable sync conflicts and loss of working context.

Візуальне порівняння графа коміктів (Merge vs Rebase)
Visual comparison of commit graph (Merge vs Rebase)

Git Merge Graph:
A---B---C (main)
     \
      D---E (feature) -> merge -> A---B---C---M (main, feature merged with commit M)

Git Rebase Graph:
A---B---C (main)
     \
      D---E (feature) -> rebase -> A---B---C---D'---E' (feature rebased on top of main)
  
💡 Простими словами: git merge об'єднує дві гілки, створюючи окремий комікт злиття і зберігаючи історію розгалужень незмінною. Тим часом git rebase переносить ваші комікти на верхівку іншої гілки, роблячи історію лінійною, але змінюючи хеші коміктів.
💡 In plain words: git merge combines two branches, creating a dedicated merge commit while keeping the branching history intact. Meanwhile, git rebase transplants your commits onto the tip of another branch, making the history linear, though it alters commit hashes.
🗣 Як читати вголосОсновна різниця між цими двома командами полягає в тому, як вони керують історією коміктів у проєкті. Коли я використовую git merge, Git створює спеціальний комікт злиття, який поєднує дві гілки, зберігаючи повну хронологію того, хто і коли вносив зміни. Це ідеально підходить для спільних гілок на зразок main, оскільки ніхто у команді не втрачає контекст розробки. З іншого боку, git rebase переносить мої локальні комікти на саму верхівку цільової гілки, створюючи чисту лінійну історію без зайвих коміктів злиття. Я зазвичай застошую rebase локально під час роботи над фічею, щоб прибрати локальне «сміття» перед створенням Pull Request. Проте я ніколи не роблю rebase на спільних гілках, бо це перезаписує історію та створює серйозні проблеми для решти команди.
🗣 How to say it out loudThe main difference between these two commands lies in how they manage commit history in a project. When I use git merge, Git creates a special merge commit that combines two branches while preserving the full timeline of who made changes and when. This is ideal for shared branches like main because no one on the team loses the development context. On the other hand, git rebase replays my local commits on top of the target branch, creating a clean linear history without extra merge commits. I typically use rebase locally while working on a feature to clean up local clutter before opening a Pull Request. However, I never rebase shared branches because it rewrites history and creates serious issues for the rest of the team.
38 Що таке git stash? What is git stash?
Збірка, залежності та Git Build, dependencies and Git junior ▶

Що це — git stash — це вбудована команда в Git, яка дозволяє тимчасово зберегти (заховати) незавершені зміни у вашій робочій директорії (working directory) та в зоні індексування (staging area), повернувши репозиторій до чистого стану останнього коміту. Це надзвичайно корисно, коли ви працюєте над завданням або проводите локальне тестування, і раптово виникає потреба переключитися на іншу гілку (наприклад, щоб терміново перевірити гарячий фікс у prod-гілці) без необхідності робити сирий або незавершений коміт.

What is it — git stash is a built-in Git command that allows you to temporarily save (stash) uncommitted changes in your working directory and staging area, returning your repository to a clean state matching the latest commit. This is extremely useful when you are working on a task or running local testing and suddenly need to switch to another branch (for example, to urgently check a hotfix on the prod branch) without having to make a raw or unfinished commit.

Навіщо/коли — Механізм git stash працює за принципом стеку (LIFO — Last In, First Out). Коли ви виконуєте команду, Git створює спеціальні внутрішні коміти, які ізолюють ваші незавершені зміни, а потім очищує робочу зону. Ви можете зберігати кілька "стешів" одночасно, і вони будуть нумеруватися в порядку черги (наприклад, stash@{0}, stash@{1}). Щоб повернути зміни назад, ви використовуєте команду git stash pop (яка застосовує зміни та видаляє їх зі стеку) або git stash apply (яка застосовує зміни, але залишає їх у стеку на випадок повторного використання).

Why/when — The git stash mechanism works on a stack principle (LIFO — Last In, First Out). When you run the command, Git creates special internal commits that isolate your uncommitted changes and then cleans your working directory. You can save multiple stashes at the same time, and they are indexed in order (e.g., stash@{0}, stash@{1}). To bring the changes back, you use git stash pop (which applies the changes and removes them from the stack) or git stash apply (which applies the changes but keeps them in the stack for reuse).

Приклад з проєкту — Цю команду варто використовувати для швидких і короткострокових перемикань контексту: наприклад, коли треба стягнути останні зміни з віддаленого репозиторію через git pull, а ваші локальні файли конфігурації конфліктують із ними. Проте git stash — це не заміна постійним комітам. Не варто тримати робочий стан у схованці тижнями чи накопичувати десятки стешів, оскільки ви легко можете заплутатися в тому, що саме в них зберігається, і втратити важливі напрацювання через випадкове очищення сховища.

Project example — This command should be used for quick, short-term context switches: for example, when you need to pull the latest changes from a remote repository via git pull, and your local configuration files conflict with them. However, git stash is not a replacement for regular commits. You should not keep your workspace in a stashed state for weeks or accumulate dozens of stashes, as you can easily get confused about what is stored in them and lose important work due to accidental clearing of the stash.

Підводні камені — Уявіть ситуацію в реальному QA-проєкті: ви ретельно налаштовуєте локальне середовище автоматизації тестування на Python/Selenium і змінюєте тестові дані. Раптово тімлід просить вас терміново перевірити критичний біг на іншій гілці (main), де стався збій збірки. Якщо ви спробуєте перемкнутися за допомогою git checkout main, Git видасть помилку через незбережені зміни. Замість того, щоб робити беззмістовний коміт на зразок "temp changes", ви виконуєте git stash, спокійно переходите на main, проводите перевірку, повертаєтеся на свою гілку й відновлюєте середовище командою git stash pop.

Pitfalls — Imagine a real-world QA project scenario: you are carefully configuring a local Python/Selenium test automation environment and modifying test data. Suddenly, your tech lead asks you to urgently check a critical test run on another branch (main) where a build failure occurred. If you try to switch branches using git checkout main, Git will throw an error due to uncommitted changes. Instead of making a meaningless commit like "temp changes", you run git stash, smoothly switch to main, perform your checks, return to your branch, and restore your environment with git stash pop.

Уточнююче питання — Найпоширеніша помилка новачків — забувати про існування накопичених стешів і застосовувати git stash pop на зовсім іншій гілці, що часто призводить до складних конфліктів злиття. Інша небезпечна помилка — занадто довге використання схованки без прив'язки до змістовних повідомлень (невикористання описових назв), через що згодом доводиться розгортати кожен стеш наосліп. Також важливо пам'ятати, що нові (не відстежувані гітом) файли за замовчуванням не потрапляють у стеш, якщо не використати прапорець -u.

Follow-up question — The most common beginner mistake is forgetting about accumulated stashes and applying git stash pop on a completely different branch, which often leads to complex merge conflicts. Another dangerous mistake is using stashes for too long without descriptive messages, making it hard to identify what each stash contains later. It is also important to remember that new untracked files are not stashed by default unless you use the -u flag.

Приклад базових команд Git Stash для збереження та відновлення змін
Example of basic Git Stash commands for saving and restoring changes
# Перевірити поточний статус робочої директорії
git status

# Зберегти незавершені зміни у стеш з описовим повідомленням
git stash save "WIP: fix login page locators for regression tests"

# Переглянути список усіх збережених стешів
git stash list

# Відновити зміни з останнього стешу та видалити його зі списку
git stash pop

# АБО відновити конкретний стеш без його видалення зі стеку
git stash apply stash@{1}
# Check the working directory status
git status

# Save unfinished changes to the stash with a descriptive message
git stash save "WIP: fix login page locators for regression tests"

# List all saved stashes
git stash list

# Restore the latest stash and drop it from the list
git stash pop

# OR apply a specific stash without removing it from the stack
git stash apply stash@{1}
💡 Простими словами: git stash — це віртуальна кишеня у Git, куди можна тимчасово скласти свої незбережені напрацювання, щоб навести лад у репозиторії, переключитися на інше завдання, а потім дістати їх назад у тому ж стані, не засмічуючи історію зайвими комітами.
💡 In plain words: git stash is like a virtual pocket in Git where you can temporarily put your uncommitted work to clean up your repository, switch to another task, and then pull it right back out in the exact same state without cluttering your history with garbage commits.
🗣 Як читати вголос Git stash — це надзвичайно зручний інструмент для тимчасового збереження незавершених змін, коли мені потрібно швидко перемкнутися між гілками. Наприклад, якщо я пишу тест або зміню якісь конфіги, але раптово треба перевірити щось на іншій гілці, я можу просто виконати 'git stash', щоб очистити робочу директорію. Під капотом воно працює за принципом стеку, ізолюючи мої зміни у вигляді спеціальних тимчасових об'єктів. Коли я закінчую інші справи, я повертаюся на свою гілку і викликаю 'git stash pop', щоб повернути все на свої місця. Головне — не зловживати цим і не накопичувати занадто багато старих стешів, щоб потім не заплутатися у конфліктах.
🗣 How to say it out loud Git stash is a super handy tool for temporarily saving uncommitted changes when I need to quickly switch between branches. For instance, if I'm writing a test or modifying configs, but suddenly need to check something on another branch, I can simply run 'git stash' to clean my working directory. Under the hood, it works on a stack principle, isolating my changes into special temporary objects. Once I'm done with other things, I return to my branch and call 'git stash pop' to restore everything back to normal. The main thing is not to abuse it or accumulate too many old stashes so I don't get tangled up in conflicts later.
39 Що таке git cherry-pick? What is git cherry-pick?
Збірка, залежності та Git Build, dependencies and Git mid ▶

Що це — Git cherry-pick — це команда, яка дозволяє застосувати конкретний коміт з однієї гілки до вашої поточної активної гілки, замість того щоб виконувати повне злиття (merge) або перебазування (rebase) всієї гілки. Це надзвичайно корисний інструмент у повсякденній роботі як розробників, так і QA-інженерів, коли потрібно точково перенести виправлення чи зміну.

What is it — Git cherry-pick is a command that allows you to apply a specific commit from one branch to your current active branch, instead of performing a full merge or rebase of the entire branch. This is an extremely useful tool in the daily workflow of both developers and QA engineers when a targeted fix or change needs to be ported over.

Навіщо/коли — Під капотом git cherry-pick бере зміни (diff), запроваджені вказаним комітом, і намагається накласти їх на поточну гілку як новий коміт. Оскільки створюється новий коміт, він матиме інший хеш (SHA-1), хоча міститиме ідентичні зміни в коді. Git використовує алгоритм тристороннього злиття; якщо змінені рядки не конфліктують із поточним станом гілки, новий коміт створюється автоматично.

Why/when — Under the hood, git cherry-pick takes the diff introduced by the specified commit and tries to apply it to the current branch as a brand-new commit. Because a new commit is created, it will have a different SHA-1 hash, even though it contains identical code changes. Git uses a three-way merge algorithm; if the changed lines do not conflict with the current state of the branch, the new commit is created automatically.

Приклад з проєкту — У реальних проєктах cherry-pick найчастіше використовується для термінових хотфіксів (hotfixes). Наприклад, розробник зафіксив критичний дефект у гілці develop або іншій фіч-гілці, а QA-інженеру чи тіді потрібно швидко перевірити це виправлення на середовищі релізу, де решта нових функцій ще не готова до викатки. Замість мерджити всю сиру гілку develop у release, ви просто «висмикуєте» один конкретний коміт із багфіксом.

Project example — In real projects, cherry-pick is most commonly used for urgent hotfixes. For example, a developer fixes a critical defect in the develop branch or a feature branch, and a QA engineer or lead needs to quickly test this fix on a release environment where the rest of the new features are not yet ready for deployment. Instead of merging the entire unstable develop branch into release, you simply "pluck" that single specific commit containing the bug fix.

Підводні камені — Головна поширена помилка при використанні cherry-pick — це зловживання ним замість повноцінного мерджу, що призводить до дублювання комітів та «засмічення» історії репозиторію. Також часто виникають складні конфлікти, якщо коміт, що копіюється, залежить від інших попередніх змін, яких немає на цільовій гілці. Якщо ви робите cherry-pick, важливо розуміти контекст коду і стежити за тим, щоб не переносити зайві залежності.

Pitfalls — A common mistake when using cherry-pick is abusing it instead of a proper merge, which leads to duplicate commits and clutters the repository history. Complex conflicts also frequently occur if the copied commit depends on prior changes that are missing on the target branch. If you perform a cherry-pick, it is crucial to understand the code context and ensure you aren't accidentally bringing over unintended dependencies.

Уточнення на співбесіді — Чим cherry-pick ризикований для релізних гілок? Коміт отримує новий хеш, і якщо він залежить від інших змін, у релізі може опинитися неповна правка.

Interview follow-up — Why is cherry-pick risky for release branches? The commit gets a new hash, and if it depends on other changes, the release may end up with an incomplete fix.

Приклад використання команди git cherry-pick та вирішення конфліктів
Example of using git cherry-pick command and resolving conflicts
# 1. Переходимо на цільову гілку (наприклад, релізну), куди потрібно перенести виправлення
git checkout release

# 2. Виконуємо cherry-pick конкретного коміту за його хешем з іншої гілки
git cherry-pick a1b2c3d4

# 3. Якщо виникають конфлікти, редагуємо файл, додаємо його до індексу та продовжуємо:
git add src/components/PaymentModal.js
git cherry-pick --continue

# Або, якщо потрібно скасувати процес cherry-pick у разі тупика:
git cherry-pick --abort
# 1. Switch to the target branch (e.g. the release branch) that needs the fix
git checkout release

# 2. Cherry-pick a specific commit by its hash from another branch
git cherry-pick a1b2c3d4

# 3. On conflicts, edit the file, add it to the index and continue:
git add src/components/PaymentModal.js
git cherry-pick --continue

# Or, to abort the cherry-pick if you are stuck:
git cherry-pick --abort
💡 Простими словами: git cherry-pick — це як висмикнути лише одну конкретну сторінку з чужого блокнота і вклеїти у свій, замість того щоб переписувати чи копіювати всю книжку. Це незамінно, коли потрібен терміновий багфікс з іншої робочої гілки прямо зараз, але зливати всю гілку ще зарано чи небезпечно.
💡 In plain words: git cherry-pick is like plucking a single specific page out of someone else's notebook and pasting it into yours, instead of copying or merging the entire book. It's invaluable when you need an urgent bug fix from another working branch right now, but merging the whole branch is still too premature or risky.
🗣 Як читати вголос Я часто використовую git cherry-pick, коли виникає потреба перенести точковий коміт із виправленням бага в іншу гілку без злиття всього репозиторію. Наприклад, у нас на проєкті розробник зафіксив критичний дефект у гілці розробки, а мені як тестувальнику потрібно терміново перевірити його на релізному середовищі, куди інші сирі фічі ще не повинні потрапити. Я копіюю хеш цього коміту, перемикаюся на гілку релізу і виконую команду git cherry-pick із цим хешем. Git створює новий коміт із тими ж самими змінами на моїй поточній гілці. Звісно, іноді трапляються конфлікти коду, якщо цільова гілка занадто відрізняється, але вони вирішуються стандартним шляхом. Головне — не перетворювати cherry-pick на постійну заміну звичайному мерджу, щоб не заплутати історію дублями.
🗣 How to say it out loud I frequently use git cherry-pick when I need to apply a targeted bug-fix commit to another branch without merging the entire repository. For instance, on our project, a developer fixes a critical defect in a feature branch, and I as a QA engineer need to verify it immediately on the release environment where other raw features shouldn't be deployed yet. I copy the hash of that commit, switch to the release branch, and run git cherry-pick with that hash. Git then generates a brand-new commit containing those exact changes on my current branch. Of course, code conflicts can occasionally happen if the target branch has diverged significantly, but they are resolved just like regular merge conflicts. The main rule is to avoid making cherry-pick a permanent substitute for regular merging, so we don't clutter the git history with duplicates.
40 Що таке .gitignore? What is .gitignore?
Збірка, залежності та Git Build, dependencies and Git junior ▶

Що це — .gitignore — це спеціальний текстовий файл, який розміщується в корені репозиторію та містить список папок і файлів, які Git повинен повністю ігнорувати і не відстежувати. Завдяки цьому файлу локальні артефакти, системні кеші та чутливі дані не потрапляють у віддалений репозиторій (наприклад, на GitHub чи GitLab), зберігаючи історію комітів чистою та безпечною для всієї команди.

What is it — .gitignore is a special text file located at the root of a repository that contains a list of directories and files that Git should completely ignore and not track. Thanks to this file, local artifacts, system caches, and sensitive data never reach the remote repository (such as GitHub or GitLab), keeping the commit history clean and safe for the entire team.

Навіщо/коли — Механізм роботи дуже простий: коли ви виконуєте команди на кшталт git status або git add ., Git спочатку перевіряє правила, прописані у .gitignore. Якщо назва чи шлях файлу збігаються з маскою у цьому файлі (наприклад, *.log або node_modules/), Git просто ігнорує їх під час сканування робочої директорії, і вони ніколи не стають кандидатами на створення коміту.

Why/when — The underlying mechanism is straightforward: when you run commands like git status or git add ., Git first checks the patterns defined in .gitignore. If a file name or path matches a rule inside this file (for example, *.log or node_modules/), Git simply ignores them while scanning the working directory, and they never become candidates for a commit.

Приклад з проєкту — У повсякденній роботі QA-інженера (як ручного, так і автоматизатора) до .gitignore обов'язково додають: середовища розробки (наприклад, папки .idea/ для IntelliJ IDEA чи .vscode/), файли конфігурації з паролями та токенами (.env), а також результати виконання тестів (папки зі звітами Allure, Playwright або Cypress, лог-файли та тимчасові дампи бази даних).

Project example — In the daily work of a QA engineer (both manual and automation), .gitignore typically includes: IDE settings (such as .idea/ for IntelliJ IDEA or .vscode/), configuration files containing passwords and tokens (.env), and test execution results (folders with Allure, Playwright, or Cypress reports, log files, and temporary database dumps).

Підводні камені — Типовий реальний сценарій: під час запуску локальних UI-тестів автоматизатор генерує тисячі скріншотів падінь та важких HTML-звітів загальним обємом у сотні мегабайт. Якщо не додати ці папки до .gitignore, вони випадково потраплять у Pull Request, роздують розмір репозиторію та створять величезну кількість зайвих змін, які нікому не потрібні на віддаленому сервері. З іншого боку, не варто додавати до .gitignore файли конфігурації-шаблони (на кшталт .env.example), які необхідні іншим членам команди для старту проєкту.

Pitfalls — A typical real-world scenario: while running local UI tests, an automation engineer generates thousands of failure screenshots and heavy HTML reports totaling hundreds of megabytes. If these folders are not added to .gitignore, they will accidentally end up in a Pull Request, bloat the repository size, and create massive unnecessary changes that nobody needs on the remote server. On the other hand, you should never ignore configuration templates (like .env.example) that other team members need to bootstrap the project.

Уточнююче питання — Найпоширеніша помилка новачків — спроба додати файл у .gitignore вже після того, як він був зафіксований (закомічений) у Git. Справа в тому, що .gitignore не діє на файли, які Git вже відстежує (tracked). У такому разі спочатку потрібно видалити файл з індексу за допомогою команди git rm --cached , а вже потім додати його маску до файлу ігнорування.

Follow-up question — The most common mistake beginners make is trying to add a file to .gitignore after it has already been tracked (committed) in Git. The thing is, .gitignore does not affect files that Git is already tracking. In such cases, you must first remove the file from the tracking index using the command git rm --cached and only then add its pattern to the ignore file.

Приклад типового файлу .gitignore для проєкту з QA-автоматизації
Example of a typical .gitignore file for a QA Automation project
# Dependencies
node_modules/
vendor/

# Test execution artifacts and reports
allure-results/
allure-report/
playwright-report/
test-results/
cypress/videos/
cypress/screenshots/

# Logs and databases
logs/
*.log
npm-debug.log*
*.db

# IDE and OS specific files
.idea/
.vscode/
*.iml
.DS_Store
Thumbs.db

# Environment variables and sensitive secrets
.env
secrets.json
credentials/
💡 Простими словами: .gitignore — це своєрідний «розклад заборон» для Git. Ви прописуєте там правила, які файли не повинні потрапляти під контроль версій, щоб репозиторій залишався чистим і вільним від сміття на кшталт логів, звітів тестування чи чутливих паролів.
💡 In plain words: .gitignore is essentially a "blacklist" for Git. You define rules specifying which files should never be tracked, keeping the repository clean and free from clutter like logs, test reports, or sensitive passwords.
🗣 Як читати вголос .gitignore — це текстовий файл у корені репозиторію, який говорить Git, які файли та папки слід ігнорувати і не відстежувати. У своїй роботі я зазвичай додаю туди системні файли на кшталт .DS_Store, налаштування IDE на кшталт .idea, а також результати запуску тестів і файли логів. Дуже важливо розуміти, що якщо файл уже був зафіксований Git раніше, простого додавання його у .gitignore недостатньо — його спочатку треба видалити з індексу за допомогою команди git rm --cached. Головна мета цього файлу — тримати репозиторій чистим, уникати конфліктів через локальні налаштування та захищати секретні дані від випадкового витоку.
🗣 How to say it out loud .gitignore is a plain text file at the root of a repository that tells Git which files and directories to ignore and stop tracking. In my daily work, I typically add system files like .DS_Store, IDE configurations such as .idea, test execution artifacts, and log files to it. It's crucial to understand that if a file is already tracked by Git, simply adding it to .gitignore won't stop tracking it; you first need to remove it from the index using the git rm command. The primary goal of this file is to keep the repository clean, prevent merge conflicts caused by local settings, and protect sensitive data from accidental leaks.
▶ 4 13
41 Що таке CI (Continuous Integration)? What is CI (Continuous Integration)?
CI/CD, Docker та Архітектурні патерни CI/CD, Docker and architectural patterns junior ▶

Що це — Безперервна інтеграція (Continuous Integration, CI) — це практика розробки програмного забезпечення, під час якої розробники регулярно (часто кілька разів на день) зливають свої зміни коду в єдиний центральний репозиторій. Кожне таке злиття автоматично запускає збірку проєкту та набір базових автоматизованих перевірок, таких як юніт-тести та лінтери статичного аналізу. Основна мета CI полягає в тому, щоб якомога раніше виявити та виправити конфлікти інтеграції ще до того, як вони перетворяться на масивну проблему.

What is it — Continuous Integration (CI) is a software development practice where developers regularly merge their code changes into a central repository, often multiple times a day. Each merge automatically triggers a build and a suite of basic automated checks, such as unit tests and static code linters. The primary goal of CI is to detect and fix integration conflicts as early as possible before they snowball into massive architectural issues.

Навіщо/коли — Механізм CI базується на принципі «маленьких і частих ітерацій» замість рідкісних і масивних злиттів коду. Коли розробник надсилає (push) код у гілку репозиторію, спеціальний CI-сервер (наприклад, Jenkins, GitHub Actions або GitLab CI) ізольовано створює середовище, завантажує залежності, виконує тести та повідомляє команду про результат. Якщо збірка «червона» (тести впали), команда одразу бачить, чий саме коміт спричинив проблему, що дозволяє миттєво локалізувати та виправити помилку.

Why/when — The CI mechanism relies on the principle of "small and frequent iterations" rather than rare, massive code merges. When a developer pushes code to a repository branch, a dedicated CI server (such as Jenkins, GitHub Actions, or GitLab CI) provisions an isolated environment, downloads dependencies, runs tests, and reports the outcome to the team. If the build turns "red" (tests fail), the team immediately sees whose commit caused the issue, allowing them to pinpoint and fix the bug instantly.

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

Project example — In a real project, such as building a healthcare or financial application, the workflow looks like this: a developer adds a new payment validation module, opens a Pull Request, and the CI system automatically executes the full suite of unit tests and security scans. If even a single test fails due to a type conflict, code merging into the main branch is blocked. This guarantees that QA engineers always receive a stable and pre-checked build for testing, rather than a raw product plagued by basic syntax or logic errors.

Підводні камені — Серед найпоширеніших помилок при впровадженні CI — ігнорування «червоних» білдів (коли команда продовжує комітити код попри падіння тестів), занадто повільний пайплайн, який змушує розробників годинами чекати на результати, а також відсутність локального запуску тестів перед відправкою в репозиторій. Важливо розуміти, що CI — це не просто набір налаштувань на сервері, а перш за все інженерна культура швидкого зворотного зв'язку.

Pitfalls — Among the most common mistakes when implementing CI are ignoring "red" builds (where the team keeps committing code despite failing tests), an overly slow pipeline that forces developers to wait hours for results, and failing to run tests locally before pushing to the repository. It is crucial to understand that CI is not just a set of server configurations, but primarily an engineering culture of fast feedback.

Уточнення на співбесіді — Скільки має тривати CI-прогін на pull request? Орієнтир — до 10–15 хвилин; довші набори переносять у нічний або post-merge запуск.

Interview follow-up — How long should a CI run on a pull request take? A rough target is 10–15 minutes; longer suites move to a nightly or post-merge run.

Приклад конфігурації GitHub Actions для CI (YAML)
GitHub Actions CI configuration example (YAML)
name: CI Pipeline

on:
  pull_request:
    branches: [ main ]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm ci

      - name: Run Unit Tests
        run: npm test
💡 Простими словами: CI — це автоматизований «контролер на прохідній», який перевіряє кожну нову зміну коду на наявність багів одразу після того, як розробник додав її в спільний репозиторій, не даючи поламаному коду потрапити в основну гілку.
💡 In plain words: CI is an automated "security guard" that checks every new code change for bugs right after a developer pushes it to the shared repository, preventing broken code from reaching the main branch.
🗣 Як читати вголос Безперервна інтеграція або CI — це стандартна практика, коли ми регулярно, по кілька разів на день, об'єднуємо код у головну гілку репозиторію. Головна фішка в тому, що кожне таке об'єднання автоматично запускає білд і набір юніт-тестів на сервері, щоб ми одразу бачили, чи все працює коректно. Це рятує команду від «пекла інтеграції», коли всі пишуть код окремо тижнями, а потім намагаються його склеїти. Для QA це означає, що ми отримуємо стабільні збірки набагато раніше, а базові баги виловлюються на рівні окремих задач, а не в день релізу.
🗣 How to say it out loud Continuous Integration, or CI, is a standard practice where we regularly merge our code into the main repository branch multiple times a day. The main point is that every single merge automatically triggers a build and a suite of unit tests on a server so we can immediately see if everything works properly. This saves the team from "integration hell," where everyone codes separately for weeks and then tries to glue it all together. For QA, this means we get stable builds much earlier, and basic bugs are caught at the individual task level rather than on release day.
42 Що таке CD (Continuous Delivery)? What is CD (Continuous Delivery)?
CI/CD, Docker та Архітектурні патерни CI/CD, Docker and architectural patterns mid ▶

Що це — Безперервна доставка (Continuous Delivery, CD) — це практика розробки програмного забезпечення, яка гарантує, що ваш код завжди готовий до автоматичного або ручного розгортання в production. На відміну від Continuous Deployment, де кожна збірка йде на реальні серверні середовища напряму без участі людини, CD передбачає обов'язковий етап ручного підтвердження (approval gate) перед викатуванням змін для кінцевих користувачів.

What is it — Continuous Delivery (CD) is a software development practice that ensures your code is always in a deployable state to production. Unlike Continuous Deployment, where every passing build goes straight to production without human intervention, CD retains a mandatory manual approval gate before releasing changes to real users.

Навіщо/коли — Технічно механізм CD є продовженням CI-пайплайну: після успішного проходження юніт- та інтеграційних тестів, а також пакування коду в артефакт (наприклад, Docker-образ), система автоматично розгортає його на середовищі, ідентичному продукшену (Staging або UAT). QA-інженери, менеджери та стейкхолдери можуть проводити фінальне прийомне тестування в умовах, максимально наближених до реальних.

Why/when — Technically, the CD mechanism extends the CI pipeline: after successful unit and integration tests, along with packaging the code into an artifact (e.g., a Docker image), the system automatically deploys it to a production-like environment (Staging or UAT). QA engineers, managers, and stakeholders can perform final acceptance testing in conditions as close to reality as possible.

Приклад з проєкту — Цю практику варто використовувати в проєктах із короткими циклами випусків, SaaS-рішеннях та командах, які прагнуть уникнути стресових релізів наприкінці тижня. Проте CD не підходить для систем зі суворими регуляторними вимогами, де кожна зміна потребує тижневих аудитів до появи на пре-продакшені, або для проєктів із мінімальним і нестабільним автоматизованим тестовим покриттям.

Project example — This practice is ideal for projects with short release cycles, SaaS solutions, and teams aiming to eliminate stressful Friday releases. However, CD is not suitable for systems with strict regulatory compliance where every change requires weeks of audits before hitting pre-production, or for projects with minimal and unstable automated test coverage.

Підводні камені — У реальному проєкті електронної комерції CD дозволяє розробникам зливати фічі в main-гілку щодня. Пайплайн збирає мікросервіси, пушить образи в реєстр та оновлює QA-кластер Kubernetes. Після завершення автоматичних E2E-тестів QA-лід перевіряє результати й натискає кнопку «Approve» у Jenkins чи GitLab CI, після чого бізнес може одразу побачити нові фічі на стейджингу.

Pitfalls — In a real e-commerce project, CD allows developers to merge features into the main branch daily. The pipeline builds microservices, pushes images to a registry, and updates the QA Kubernetes cluster. Once automated E2E tests pass, the QA Lead reviews the results and clicks "Approve" in Jenkins or GitLab CI, allowing business stakeholders to immediately preview new features on staging.

Уточнююче питання — Типова помилка при впровадженні CD — фокус виключно на інструментах (ArgoCD, GitLab CI, Spinnaker) за відсутності належної культури якості. Якщо стейджинг-середовище постійно падає через зовнішні фактори або тести видають помилкові спрацьовування (flaky tests), команда швидко втрачає довіру до пайплайну і переходить до ручних збірок, що зводить нанівець саму ідею безперервної доставки.

Follow-up question — A common mistake when adopting CD is focusing purely on tools (ArgoCD, GitLab CI, Spinnaker) while neglecting quality culture. If the staging environment constantly fails due to external factors or tests produce false positives (flaky tests), the team quickly loses trust in the pipeline and reverts to manual builds, defeating the entire purpose of continuous delivery.

Фрагмент GitHub Actions пайплайну для CD-етапу
Snippet of a GitHub Actions pipeline for the CD stage
name: Production CD Pipeline

on:
  push:
    branches: [ main ]

jobs:
  test-and-build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Run Unit Tests
        run: npm test

  deploy-to-staging:
    needs: test-and-build
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - name: Deploy to Staging Cluster
        run: |
          echo "Deploying container to staging Kubernetes..."
          kubectl apply -f k8s/staging/

  manual-production-gate:
    needs: deploy-to-staging
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://app.example.com
    steps:
      - name: Wait for manual approval
        run: echo "Approved by QA/Release Manager. Proceeding to prod."
💡 Простими словами: Continuous Delivery (CD) — це стан проєкту, за якого протестований код автоматично доставляється на передпрод середовище і в будь-який момент готовий полетіти в production за допомогою натискання однієї кнопки. Це перетворює випуск оновлень на звичний і безпечний рутинний процес.
💡 In plain words: Continuous Delivery (CD) is a project state where tested code is automatically delivered to a staging environment, remaining fully ready to deploy to production at any moment with a single click. It turns software releases into a predictable, safe, and routine process.
🗣 Як читати вголосДля мене Continuous Delivery — це логічний крок після CI, коли автоматично зібраний та протестований код одразу деплоїться на стейджинг. Головна відмінність CD від Continuous Deployment полягає в тому, що фінальний крок у прод виконується через ручне підтвердження людиною. На практиці це означає, що ми постійно маємо актуальний та стабільний білд на середовищі тестування, куди зміни потрапляють щодня. Такий підхід допомагає позбутися страху перед великими релізами та дає змогу QA-команді оперативно валідувати нові фічі в умовах, близьких до бойових. Найголовніше тут — підтримувати стабільність тестового оточення, щоб автоматизація не ламалася через сторонні чинники.
🗣 How to say it out loudTo me, Continuous Delivery is the natural next step after CI, where automatically built and tested code is deployed straight to staging. The main difference between CD and Continuous Deployment is that the final step to production is executed via a manual human approval gate. In practice, this means we constantly have an up-to-date and stable build on our testing environment, where changes flow daily. This approach helps eliminate release anxiety and allows the QA team to quickly validate new features under production-like conditions. The most important thing here is maintaining test environment stability so automation doesn't break due to external factors.
43 Що таке Continuous Deployment? What is Continuous Deployment?
CI/CD, Docker та Архітектурні патерни CI/CD, Docker and architectural patterns mid ▶

Що це — Continuous Deployment (CD) — це еволюція концепції Continuous Delivery, де кожна зміна коду, яка успішно проходить автоматизовані тести, миттєво та без жодного ручного втручання розгортається у виробничому середовищі (production). Якщо при Continuous Delivery рішення про реліз ухвалює людина (натисканням кнопки), то в Continuous Deployment весь шлях від коміту до продакшену повністю автоматизований.

What is it — Continuous Deployment (CD) is an evolution of Continuous Delivery where every code change that successfully passes automated tests is instantly and automatically deployed to the production environment without any manual intervention. While Continuous Delivery requires a human decision (a manual approval click) to release, Continuous Deployment fully automates the entire path from code commit to production.

Навіщо/коли — Механізм роботи CD базується на зв'язці систем контролю версій (Git) та CI/CD-пайплайнів (наприклад, GitHub Actions, GitLab CI/CD, Jenkins). Коли розробник робить merge у гілку main, пайплайн послідовно запускає юніт-тести, інтеграційні та контрактні тести, збирає Docker-образ, завантажує його в реєстр, виконує деплой на кластер (наприклад, Kubernetes або AWS ECS) та запускає автоматичні smoke-тести.

Why/when — The CD mechanism relies on version control systems (Git) and CI/CD pipelines (such as GitHub Actions, GitLab CI/CD, or Jenkins). When a developer merges code into the main branch, the pipeline sequentially executes unit, integration, and contract tests, builds a Docker image, pushes it to a registry, deploys it to a cluster (like Kubernetes or AWS ECS), and runs automated smoke tests.

Приклад з проєкту — Цей підхід ідеально підходить для SaaS-продуктів, мікросервісної архітектури з високим покриттям тестами та команд, які активно використовують feature flags для безпечного приховування неготових фіч. Проте CD категорично не рекомендується використовувати в критичних життєзабезпечуючих системах (медицина, авіація), на легасі-проєктах із нестабільними чи довгими тестами, або за відсутності якісного моніторингу й швидких інструментів відкату (rollback).

Project example — This approach is ideal for SaaS products, microservices architectures with high test coverage, and teams actively using feature flags to safely hide unfinished features. However, CD is strictly not recommended for safety-critical systems (medical, aerospace), legacy projects with brittle or slow tests, or environments lacking robust monitoring and fast rollback capabilities.

Підводні камені — Реальний проєктний сценарій: у fintech-додатку розробник виправляє баг валідації суми переказу. Після злиття коду пайплайн збирає новий Docker-контейнер та за допомогою стратегії canary deployment оновлює 10% серверів. Система автоматично відстежує метрики помилок (наприклад, через Prometheus/Datadog) протягом п'яти хвилин; якщо аномалій не виявлено, трафік плавно переводиться на нову версію на 100% без участі QA чи DevOps.

Pitfalls — A real project scenario: in a fintech app, a developer fixes a transfer amount validation bug. After merging, the pipeline builds a new Docker container and uses a canary deployment strategy to update 10% of the servers. The system automatically monitors error rates (e.g., via Prometheus/Datadog) for five minutes; if no anomalies are detected, traffic smoothly shifts 100% to the new version without any QA or DevOps manual involvement.

Уточнююче питання — Головна помилка QA при впровадженні CD — сприймати автоматизацію як заміну тестувальника, а не якісний бар'єр (quality gate). Інша поширена проблема — надмірна довіра до високого відсотка покриття коду замість фокусування на поведінкових, регресійних та контрактних тестах, що призводить до пропуску критичних бізнес-помилок прямо у прод.

Follow-up question — A common QA mistake when implementing CD is treating automation as a replacement for testers rather than a robust quality gate. Another widespread issue is over-relying on high code coverage percentages instead of focusing on behavioral, regression, and contract tests, which allows critical business-logic bugs to slip straight into production.

Приклад конфігурації CI/CD пайплайну для Continuous Deployment (GitHub Actions)
CI/CD Pipeline configuration snippet for Continuous Deployment (GitHub Actions)
name: Production Continuous Deployment
on:
  push:
    branches: [ main ]
jobs:
  deploy-to-prod:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Source Code
        uses: actions/checkout@v3
      - name: Run Automated Test Suite
        run: npm run test:ci
      - name: Build Docker Container
        run: docker build -t myapp:${{ github.sha }} .
      - name: Push & Deploy via Kubernetes
        run: |
          docker push myapp:${{ github.sha }}
          kubectl set image deployment/app app=myapp:${{ github.sha }}
      - name: Run Post-Deployment Smoke Tests
        run: npm run test:smoke --url=https://prod.company.com
💡 Простими словами: Continuous Deployment — це повна автоматизація доставки коду від коміту до продакшену. Кожна зміна, яка успішно пройшла всі автоматизовані тести й перевірки в Docker-контейнері, миттєво стає доступною для реальних користувачів без будь-яких ручних натискань кнопки «Deploy».
💡 In plain words: Continuous Deployment is end-to-end code delivery automation from commit to production. Every change that successfully passes all automated tests and checks inside a Docker container instantly becomes available to real users without any manual "Deploy" button clicks.
🗣 Як читати вголос Continuous Deployment — це логічний розвиток Continuous Delivery, де весь процес релізу повністю позбавлений ручних кроків. У моєму розумінні, головна відмінність полягає в тому, що код автоматично летить у прод щоразу, коли проходять усі автоматизовані тести, без потреби у ручному підтвердженні менеджера чи QA. Щоб це працювало надійно, критично мати стабільне тестове покриття, включаючи юніт-, інтеграційні та post-deployment smoke-тести. На практиці ми використовували Docker-контейнеризацію та стратегію поступового розгортання на кшталт canary releases, щоб у разі виявлення проблем швидко та безпечно виконати rollback. Моя роль як QA в такому процесе полягала не в ручному тестуванні кожного білда, а в розробці надійних quality gates усередині самого пайплайну.
🗣 How to say it out loud Continuous Deployment is the logical evolution of Continuous Delivery, where the entire release process is completely stripped of manual steps. In my view, the key difference is that code automatically flies to production whenever all automated tests pass, without needing any manual sign-off from a manager or a QA. To make this work reliably, having solid test coverage—including unit, integration, and post-deployment smoke tests—is non-negotiable. In practice, we relied on Docker containerization and progressive delivery strategies like canary releases to quickly and safely execute rollbacks if anomalies arose. My role as a QA in this setup wasn't about manually testing every build, but rather designing reliable quality gates right inside the pipeline.
44 Який файл описує pipeline у GitHub Actions? Which file describes a pipeline in GitHub Actions?
CI/CD, Docker та Архітектурні патерни CI/CD, Docker and architectural patterns junior ▶

Що це — У GitHub Actions процес неперервної інтеграції та доставки (CI/CD) описується за допомогою спеціальних конфігураційних файлів, які називаються workflows (робочі процеси). Вони обов'язково мають формат YAML (.yml або .yaml) і повинні знаходитися в репозиторії за чітко визначеним шляхом: .github/workflows/.

What is it — In GitHub Actions, the continuous integration and delivery (CI/CD) process is described using special configuration files called workflows. They must be in YAML format (.yml or .yaml) and located in the repository at a specific path: .github/workflows/.

Навіщо/коли — Механізм роботи цього файлу полягає в декларативному описі подій (on), які запускають pipeline, та завдань (jobs), що виконуються на віртуальних серверах (runners). Кожен job складається з послідовності кроків (steps), де можуть запускатися як готові сторонні дії з GitHub Marketplace (наприклад, встановлення Node.js), так і власні кастомні команди термінала для збирання проєкту та запуску тестів.

Why/when — The mechanism behind this file relies on a declarative description of events (on) that trigger the pipeline, and jobs (jobs) executed on virtual servers called runners. Each job consists of a sequence of steps (steps), which can run pre-made actions from the GitHub Marketplace (like setting up Node.js) or custom terminal commands to build the project and execute tests.

Приклад з проєкту — На реальному проєкті такий файл є критично важливим для QA-інженера, оскільки він автоматично активує перевірки якості коду та виконання автотестів при кожному створенні Pull Request або злитті гілок. Наприклад, файл ci.yml може налаштовувати середовище, інсталювати залежності та запускати регресійні сценарії на Playwright або Selenium, блокуючи злиття коду (merge), якщо хоча б один тест падає.

Project example — On a real project, such a file is critical for a QA engineer because it automatically triggers code quality checks and automated tests upon every Pull Request creation or branch merge. For instance, a ci.yml file can set up the environment, install dependencies, and run regression suites on Playwright or Selenium, blocking the code merge if even a single test fails.

Підводні камені — Найпоширенішими помилками при створенні цих файлів є порушення синтаксису YAML (наприклад, використання табуляції замість пробілів, що призводить до помилок валідації), неправильне налаштування тригерів, через що pipeline не реагує на події, або ж хардкодінг чутливих даних (паролів, токенів доступу) всередині файлу замість їх безпечного зберігання у GitHub Secrets.

Pitfalls — The most common mistakes when creating these files include YAML syntax errors (such as using tabs instead of spaces, leading to validation failures), misconfigured triggers that prevent the pipeline from responding to events, or hardcoding sensitive data (passwords, access tokens) inside the file instead of securely storing them in GitHub Secrets.

Уточнення на співбесіді — Як запустити workflow вручну з параметрами? Додати тригер workflow_dispatch з inputs — наприклад, вибір середовища або набору тестів.

Interview follow-up — How do you start a workflow manually with parameters? Add a workflow_dispatch trigger with inputs — for example, choosing the environment or the test suite.

Приклад конфігураційного файлу GitHub Actions (.github/workflows/qa-tests.yml)
Example of a GitHub Actions configuration file (.github/workflows/qa-tests.yml)
name: QA Regression Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm ci

      - name: Run Playwright tests
        run: npx playwright test
💡 Простими словами: Файл pipeline у GitHub Actions — це звичайний документ у форматі YAML, який зберігається у спеціальній папці .github/workflows/ вашого репозиторію. У ньому чітко розписано три речі: коли запускати перевірку (наприклад, при кожному commit), на якій віртуальній машині та які конкретно команди (інсталяція, запуск тестів) треба виконати.
💡 In plain words: A GitHub Actions pipeline file is a regular YAML document stored in the special .github/workflows/ folder of your repository. It clearly defines three things: when to trigger the check (e.g., on every commit), which virtual machine to use, and what exact commands (installation, running tests) need to be executed.
🗣 Як читати вголос У GitHub Actions пайплайни описуються за допомогою YAML-файлів, які обов'язково зберігаються у спеціальній директорії .github/workflows/ нашого репозиторію. Я особисто працював із такими файлами, коли налаштовував автоматичний запуск регресійних тестів на базі Playwright щоразу при створенні або оновленні Pull Request. У цьому конфісі я зазвичай прописую тригери на кшталт push чи pull_request, визначаю середовище виконання через runs-on: ubuntu-latest і формую послідовність кроків: чекаут репозиторію, встановлення Node.js, інсталяція залежностей та виконання самих тестів. Головне тут — уважно ставитися до синтаксису YAML, оскільки зайвий пробіл легко зламає весь пайплайн, а також завжди використовувати GitHub Secrets для безпечного зберігання чутливих даних та токенів.
🗣 How to say it out loud In GitHub Actions, pipelines are described using YAML files that must be stored in the special .github/workflows/ directory of our repository. I've personally worked with such files when configuring the automatic execution of Playwright regression tests every time a Pull Request is created or updated. In this config, I typically specify triggers like push or pull_request, define the execution environment via runs-on: ubuntu-latest, and set up a sequence of steps: checking out the repository, setting up Node.js, installing dependencies, and running the tests themselves. The key thing here is to pay close attention to YAML syntax, as an extra space can easily break the entire pipeline, and to always use GitHub Secrets for securely storing sensitive data and tokens.
45 Як називається конфіг-файл Jenkins pipeline? What is the Jenkins pipeline config file called?
CI/CD, Docker та Архітектурні патерни CI/CD, Docker and architectural patterns junior ▶

Що це — Конфігураційний файл Jenkins Pipeline називається Jenkinsfile. Це текстовий файл, у якому описується весь життєвий цикл збірки проєкту, тестування та розгортання в рамках CI/CD-процесу. Замість налаштування задач через класичний веб-інтерфейс Jenkins (Classic UI), підхід "Pipeline as Code" дозволяє зберігати інструкції безпосередньо в репозиторії коду поруч із самим проєктом.

What is it — The Jenkins Pipeline configuration file is called a Jenkinsfile. It is a text file that defines the entire project build, test, and deployment lifecycle as part of the CI/CD process. Instead of configuring jobs through the classic Jenkins web UI, the "Pipeline as Code" approach allows storing instructions directly in the code repository alongside the project itself.

Навіщо/коли — Jenkinsfile зазвичай пишеться на основі мови Groovy та використовує один із двох синтаксисів: Declarative (більш структурований, читабельний і суворий до синтаксису) або Scripted (гнучкіший, але складніший у підтримці). Файл складається з логічних блоків — pipeline, stages, stage та steps, які послідовно виконуються агентами Jenkins. Завдяки цьому і QA-інженери, і розробники можуть бачити статус проходження збірок та тестів безпосередньо у гілках Git.

Why/when — A Jenkinsfile is typically written in Groovy and uses one of two syntaxes: Declarative (more structured, readable, and strict in syntax) or Scripted (more flexible, but harder to maintain). The file consists of logical blocks—pipeline, stages, stage, and steps—that are sequentially executed by Jenkins agents. This allows both QA engineers and developers to see the status of builds and tests directly within Git branches.

Приклад з проєкту — У реальних проєктах Jenkinsfile використовується щоразу, коли необхідно налаштувати автоматичний запуск регресійних тестів (наприклад, UI-тестів на Playwright або API-тестів на RestAssured) після кожного злиття коду в головну гілку або за розкладом (nightly builds). Наприклад, наш QA-відділ використовує Jenkinsfile для автоматичного тригера пакету регресійних тестів у Docker-контейнері одразу після того, як розробники викатують оновлення на тестове середовище (staging).

Project example — In real projects, a Jenkinsfile is used whenever it is necessary to configure automated execution of regression tests (e.g., UI tests with Playwright or API tests with RestAssured) after every code merge to the main branch or on a schedule (nightly builds). For instance, our QA team uses a Jenkinsfile to automatically trigger a regression test suite inside a Docker container right after developers roll out updates to the staging environment.

Підводні камені — Найпоширеніші помилки при роботі з Jenkinsfile — це жорстке прописування чутливих даних (паролів, токенів доступу, API-ключів) замість використання захищеного сховища Jenkins Credentials Store, а також створення занадто монолітних і довгих файлів замість винесення логіки у Shared Libraries. Початківці також часто припускаються синтаксичних помилок у Groovy або забувають налаштувати блоки post для автоматичного надсилання сповіщень у Slack чи генерації звітів на кшталт Allure.

Pitfalls — Common mistakes when working with a Jenkinsfile include hardcoding sensitive data (passwords, access tokens, API keys) instead of using the secure Jenkins Credentials Store, as well as creating overly monolithic and long files instead of extracting logic into Shared Libraries. Beginners also frequently make Groovy syntax errors or forget to configure post blocks for automatically sending notifications to Slack or generating reports like Allure.

Уточнення на співбесіді — Чим declarative pipeline відрізняється від scripted? Declarative має фіксовану структуру pipeline/stages/steps і простіший для читання, scripted — це довільний Groovy.

Interview follow-up — How does a declarative pipeline differ from a scripted one? Declarative has a fixed pipeline/stages/steps structure and is easier to read; scripted is free-form Groovy.

Приклад декларативного Jenkinsfile для запуску QA-тестів
Example of a declarative Jenkinsfile for running QA tests
pipeline {
    agent any
    stages {
        stage('Checkout Code') {
            steps {
                checkout scm
            }
        }
        stage('Install Dependencies') {
            steps {
                sh 'npm install'
            }
        }
        stage('Run Regression Tests') {
            steps {
                sh 'npm run test:regression'
            }
        }
    }
    post {
        always {
            allure includeProperties: false, results: [[path: 'allure-results']]
        }
    }
}
💡 Простими словами: Jenkinsfile — це головна інструкція для сервера Jenkins, написана у вигляді коду. Вона чітко говорить системі: "Спочатку візьми код із репозиторію, потім встанови залежності, запусти наші автотести, а наприкінці сформуй звіт про результати тестування".
💡 In plain words: A Jenkinsfile is the master instruction manual for the Jenkins server, written as code. It clearly tells the system: "First grab the code from the repository, then install dependencies, run our automated tests, and finally generate a test results report."
🗣 Як читати вголос Конфігураційний файл Jenkins pipeline називається Jenkinsfile. Це текстовий документ, який зберігається безпосередньо в репозиторії проєкту і визначає весь процес CI/CD. На практиці ми використовуємо його, щоб автоматично запускати наші регресійні та API-тести після кожного коміту або за розкладом. Мені подобається цей підхід, бо він реалізує концепцію Pipeline as Code — тобто налаштування збірок лежать під контролем версій, і кожен член команди може їх переглянути чи оновити. Головне — не хардкодити туди паролі чи токени та тримати структуру зрозумілою за допомогою модульних етапів.
🗣 How to say it out loud The Jenkins pipeline configuration file is called a Jenkinsfile. It is a text document stored directly in the project repository that defines the entire CI/CD process. In practice, we use it to automatically trigger our regression and API tests after every commit or on a schedule. I like this approach because it implements the Pipeline as Code concept—meaning build configurations are under version control, and any team member can review or update them. The main thing is not to hardcode passwords or tokens in there and keep the structure clean using modular stages.
46 Що таке Docker у контексті тестування? What is Docker in the context of testing?
CI/CD, Docker та Архітектурні патерни CI/CD, Docker and architectural patterns junior ▶

Що це — У контексті тестування Docker — це технологія віртуалізації на рівні операційної системи, яка дозволяє пакувати додаток та всі його залежності в ізольовані контейнери. Для QA-інженера це означає можливість запускати тести, тестові середовища та самі досліджувані програми у повністю контрольованих умовах, які є ідентичними на будь-якому комп'ютері чи сервері без потреби складного ручного налаштування.

What is it — In the context of testing, Docker is an operating system-level virtualization technology that packs an application and all its dependencies into isolated containers. For a QA engineer, this means the ability to run tests, test environments, and the applications under test in fully controlled conditions that remain identical across any computer or server without complex manual setup.

Навіщо/коли — Технічно Docker працює за рахунок спільного використання ядра хост-системи, що робить контейнери надзвичайно легкими та швидкими у запуску на відміну від класичних віртуальних машин. Кожен контейнер містить лише те, що потрібно для роботи конкретного сервісу — наприклад, певну версію Node.js або бази даних PostgreSQL. Це гарантує, що поведінка додатка під час тестування буде передбачуваною і повністю відповідатиме продакшену.

Why/when — Technically, Docker works by sharing the host system's kernel, making containers extremely lightweight and fast to start compared to traditional virtual machines. Each container contains only what is needed for a specific service to run—such as a specific version of Node.js or a PostgreSQL database. This ensures that the application's behavior during testing is predictable and matches production.

Приклад з проєкту — У реальних проєктах Docker використовується всюди: від локального запуску середовища для ручного тестування до автоматизованих пайплайнів у CI/CD. Наприклад, якщо розробники випустили мікросервісну архітектуру, QA може підняти весь стек із кількох сервісів однією командою docker-compose up, виконати дим-тести чи автоматизовані перевірки, а після завершення повністю очистити середовище, не залишаючи конфліктів у системі.

Project example — In real projects, Docker is used everywhere: from local environment setup for manual testing to automated pipelines in CI/CD. For example, if developers release a microservices architecture, a QA can spin up the entire stack of several services with a single docker-compose up command, run smoke tests or automated checks, and completely clean up the environment afterward without leaving conflicts on the system.

Підводні камені — Найпоширенішими помилками при використанні Docker у тестуванні є ігнорування концепції незмінності (намагання «лагодити» баги прямо всередині запущеного контейнера замість перезапуску зображення), відсутність очищення невикористаних томів та кешу, що швидко забиває дисковий простір, а також неправильне налаштування мережі між контейнерами тест-раннера та додатку.

Pitfalls — The most common mistakes when using Docker in testing include ignoring immutability (trying to "fix" bugs directly inside a running container instead of restarting the image), failing to clean up unused volumes and caches which quickly exhausts disk space, and improper network configuration between the test runner and application containers.

Уточнення на співбесіді — Чим контейнер відрізняється від віртуальної машини для тестів? Контейнер ділить ядро хоста і стартує за секунди, VM має власну ОС і важча.

Interview follow-up — How does a container differ from a virtual machine for tests? A container shares the host kernel and starts in seconds, a VM has its own OS and is heavier.

Приклад Docker Compose конфігурації для тестового оточення
Example Docker Compose configuration for a test environment
version: '3.8'
services:
  app:
    image: my-test-app:latest
    ports:
      - "8080:8080"
    environment:
      - DB_HOST=db
  db:
    image: postgres:14-alpine
    environment:
      - POSTGRES_DB=test_db
      - POSTGRES_USER=qa_user
      - POSTGRES_PASSWORD=secret
💡 Простими словами: Docker для тестувальника — це як стандартизована коробка з усіма необхідними програмами та базами даних, яку можна запустити на будь-якому комп'ютері за кілька секунд. Він повністю усуває вічну проблему «на моєму комп'ютері все працює, а в тестувальника паде», гарантуючи ідеальну ідентичність середовищ.
💡 In plain words: Docker for a QA engineer is like a standardized box containing all necessary apps and databases that can be launched on any computer in seconds. It completely eliminates the eternal "it works on my machine but fails for QA" issue by guaranteeing environmental consistency.
🗣 Як читати вголос У контексті тестування Docker — це незамінний інструмент, який дозволяє запускати додатки та середовища в ізольованих контейнерах. Головна перевага для мене як QA полягає в тому, що я можу підняти точну копію продакшену або необхідну базу даних однією командою, не витрачаючи години на ручне встановлення залежностей. Наприклад, на минулому проєкті ми використовували Docker Compose, щоб автоматично розгортати мікросервіси та базу даних прямо в CI/CD пайплайні перед прогоном автоматизованих регресійних тестів. Це повністю позбавило нас плаваючих багів, пов'язаних із різними версіями бібліотек чи середовищ. Звісно, важливо знати базові команди, слідкувати за очищенням дисків від старих контейнерів і правильно конфігурувати томи, але загалом Docker робить процес тестування набагато стабільнішим і передбачуванішим.
🗣 How to say it out loud In the context of testing, Docker is an essential tool that allows me to run applications and environments inside isolated containers. The main benefit for me as a QA is that I can spin up an exact replica of production or a required database with a single command, without spending hours manually installing dependencies. For example, on my previous project, we used Docker Compose to automatically deploy microservices and a database right inside the CI/CD pipeline before running automated regression tests. This completely eliminated flaky bugs related to mismatched library versions or environments. Of course, it is important to know basic commands, clean up old disk containers, and configure volumes properly, but overall, Docker makes the testing process much more stable and predictable.
47 Що таке Dockerfile? What is a Dockerfile?
CI/CD, Docker та Архітектурні патерни CI/CD, Docker and architectural patterns junior ▶

Що це — Dockerfile — це текстовий документ, який містить послідовність інструкцій (команд), необхідних для автоматичного створення (збірки) Docker-образу. Він виступає у ролі чіткого рецепта або креслення, у якому зазначено, яка саме базова операційна система, які системні бібліотеки, залежності та файли застосунку мають бути включені в ізольоване середовище виконання.

What is it — A Dockerfile is a text document containing a sequence of instructions (commands) required to automatically build a Docker image. It acts as a clear blueprint or recipe specifying which base operating system, system libraries, dependencies, and application files must be included in the isolated execution environment.

Навіщо/коли — Механізм роботи Dockerfile базується на шаруватій архітектурі (layers). Кожна інструкція у файлі (наприклад, FROM, RUN, COPY) створює новий незмінний шар поверх попереднього, що забезпечує високу ефективність кешування: якщо інструкції не змінювалися, Docker пропускає їх і використовує вже готові шари, суттєво прискорюючи повторну збірку.

Why/when — The mechanism behind a Dockerfile relies on a layered architecture. Each instruction in the file (such as FROM, RUN, COPY) creates a new immutable layer on top of the previous one, ensuring high caching efficiency: if instructions haven't changed, Docker skips them and uses pre-cached layers, significantly speeding up rebuild times.

Приклад з проєкту — У контексті тестування та QA Dockerfile використовується для створення однакових середовищ виконання автоматизованих тестів (наприклад, запуск Selenium Grid, Cypress або API-тестів усередині контейнерів). Це повністю вирішує проблему «у мене на машині все працює, а на тест-стенді падає», оскільки кожен інженер запускає ідентичний білд.

Project example — In the context of testing and QA, a Dockerfile is used to create consistent environments for running automated tests (e.g., executing Selenium Grid, Cypress, or API tests inside containers). This completely resolves the "it works on my machine but fails on the test server" issue, as every engineer runs an identical build.

Підводні камені — У реальному проєкті QA-інженер або DevOps створює Dockerfile для автоматизації: наприклад, визначає базовий образ Python, копіює файл залежностей requirements.txt, встановлює їх через pip, копіює решту тестового фреймворку та задає команду CMD для запуску тестів через pytest безпосередньо в конвеєрі CI/CD.

Pitfalls — In a real project, a QA engineer or DevOps specialist creates a Dockerfile for test automation: for instance, defining a base Python image, copying the requirements.txt dependency file, installing them via pip, copying the rest of the test framework, and setting a CMD instruction to run tests via pytest directly inside the CI/CD pipeline.

Уточнююче питання — Поширеною помилкою початківців є ігнорування файлу .dockerignore та копіювання зайвих важких файлів (наприклад, локальних логів або папок з артефактами збірки) за допомогою COPY . ., що призводить до роздування розміру фінального образу, сповільнення збірки та потенційних вразливостей безпеки.

Follow-up question — A common mistake among beginners is ignoring the .dockerignore file and copying unnecessary heavy files (such as local logs or build artifact folders) using COPY . ., which bloats the final image size, slows down the build process, and introduces potential security vulnerabilities.

Приклад базового Dockerfile для запуску QA-автоматизації
Example of a basic Dockerfile for running QA automation
FROM python:3.10-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["pytest", "-v"]
💡 Простими словами: Dockerfile — це кулінарний рецепт у текстовому вигляді, за яким система автоматично готує для вас повністю налаштований віртуальний контейнер з усіма потрібними програмами й бібліотеками для роботи застосунку чи тестів.
💡 In plain words: A Dockerfile is like a text-based culinary recipe that tells the system how to automatically prepare a fully configured virtual container with all required software and libraries for your application or tests.
🗣 Як читати вголос Dockerfile — це текстовий документ із набором інструкцій, який використовується для автоматичного створення Docker-образів. У ньому прописується все необхідне для роботи середовища: від базової операційної системи до бібліотек і команд запуску. Я часто стикаюся з цим на практиці, коли потрібно налаштувати стабільне середовище для запуску автотестів або перевірки збірок у CI/CD. Головна перевага Dockerfile полягає в тому, що він гарантує ідентичність оточення на будь-якій машині, повністю виключаючи конфлікти залежностей. При цьому важливо пам'ятати про правильний порядок інструкцій для ефективного кешування та використання файлу .dockerignore.
🗣 How to say it out loud A Dockerfile is a text document with a set of instructions used to automatically build Docker images. It specifies everything required for an environment to run, ranging from the base operating system to libraries and startup commands. I frequently encounter this in practice when setting up a stable environment to run automated tests or verify builds in CI/CD. The main advantage of a Dockerfile is that it guarantees environment consistency across any machine, completely eliminating dependency conflicts. At the same time, it is important to keep the correct instruction order in mind for efficient caching and to utilize the .dockerignore file.
48 Що таке docker-compose? What is docker-compose?
CI/CD, Docker та Архітектурні патерни CI/CD, Docker and architectural patterns mid ▶

Що це — docker-compose — це інструмент для визначення та запуску багатоконтейнерних Docker-додатків за допомогою єдиного файлу конфігурації у форматі YAML. У контексті тестування та забезпечення якості (QA), він дозволяє QA-інженерам однією командою підняти повне середовище додатка: веб-сервер, базу даних, кешування та сторонні сервіси-залежності.

What is it — docker-compose is a tool for defining and running multi-container Docker applications using a single YAML configuration file. In the context of testing and quality assurance (QA), it allows QA engineers to spin up a complete application environment with a single command: a web server, a database, caching layers, and external service dependencies.

Навіщо/коли — Технічно засіб зчитує файл docker-compose.yml, де описуються сервіси, мережі (networks) та volumes (сховища даних), а потім використовує Docker API для створення ізольованих інфраструктурних елементів. Механізм базується на створенні віртуальної мережі обміну даними між контейнерами, що дозволяє їм взаємодіяти за іменами сервісів замість складних IP-адрес, значно спрощуючи розгортання тестових стендів.

Why/when — Technically, the tool reads the docker-compose.yml file, which describes services, networks, and data volumes, and then uses the Docker API to create isolated infrastructure components. The mechanism relies on establishing a virtual internal network between containers, allowing them to communicate via service names instead of complex IP addresses, significantly simplifying the deployment of test environments.

Приклад з проєкту — docker-compose незамінний для локального тестування, регресійного тестування перед релізом, а також для автоматизації інтеграційних та E2E тестів у CI/CD пайплайнах. Наприклад, у проєкті електронної комерції QA-інженер може запустити мок-сервер оплати, PostgreSQL та сам сервіс одночасно для перевірки оформлення замовлення в ізольованому контурі, позбувшись проблем із різницею середовищ.

Project example — docker-compose is indispensable for local testing, pre-release regression testing, and automating integration and E2E tests within CI/CD pipelines. For example, in an e-commerce project, a QA engineer can spin up a payment mock server, PostgreSQL, and the core microservice simultaneously to verify checkout flows in an isolated environment, eliminating environment discrepancy issues.

Підводні камені — Проте його не варто використовувати для масштабного продакшн-розгортання, де потрібні складні оркестратори на кшталт Kubernetes з його автоскейлінгом, балансуванням навантаження та політиками самовідновлення. Поширеною помилкою серед тестувальників є збереження чутливих даних (паролів, API-ключів) безпосередньо у файлі конфігурації замість використання змінних середовища (.env), а також ігнорування механізмів очищення volumes, що призводить до захаращення дискового простору застарілими тестовими даними.

Pitfalls — However, it should not be used for large-scale production deployments that require complex orchestrators like Kubernetes with autoscaling, load balancing, and self-healing policies. A common mistake among testers is hardcoding sensitive data (passwords, API keys) directly into the configuration file instead of using environment variables (.env), as well as ignoring volume cleanup mechanisms, which leads to disk space clutter from outdated test data.

Уточнення на співбесіді — Чому depends_on не гарантує, що сервіс готовий? Він чекає лише старту контейнера; для готовності потрібні healthcheck і condition: service_healthy.

Interview follow-up — Why doesn't depends_on guarantee a service is ready? It only waits for the container to start; readiness needs a healthcheck and condition: service_healthy.

Приклад файлу docker-compose.yml для тестового середовища
Example docker-compose.yml file for a test environment
version: '3.8'
services:
  web-app:
    image: my-app:latest
    ports:
      - "8080:80"
    environment:
      - DB_HOST=database
    depends_on:
      - database
  database:
    image: postgres:14-alpine
    environment:
      - POSTGRES_DB=test_db
      - POSTGRES_USER=qa_user
      - POSTGRES_PASSWORD=secret
    volumes:
      - db_data:/var/lib/postgresql/data
volumes:
  db_data:
💡 Простими словами: docker-compose — це як режисер театру для ваших контейнерів: ви даєте йому один сценарій (файл конфігурації), і він запускає всі декорації, акторів та сервіси (веб-додаток, базу даних, кеш) у правильному порядку однією командою.
💡 In plain words: docker-compose is like a theater director for your containers: you give it a single script (configuration file), and it launches all the props, actors, and services (web app, database, cache) in the correct order with a single command.
🗣 Як читати вголос Я активно використовую docker-compose у своїй QA-роботі, особливо коли потрібно швидко розгорнути повне середовище додатка для локального тестування або запуску автоматизованих E2E тестів. У мене є стандартний конфігураційний файл, який піднімає веб-сервіс разом із потрібною базою даних і тестовими моками однією командою docker-compose up. Це рятує від головного болю з налаштуванням залежностей вручну та дозволяє тестувати в умовах, максимально наближених до реального використання. Головне — не забувати керувати кешем сховищ даних і виносити секрети у змінні середовища.
🗣 How to say it out loud I actively use docker-compose in my QA work, especially when I need to quickly spin up a complete application environment for local testing or running automated E2E tests. I have a standard configuration file that starts the web service along with the required database and test mocks using a single docker-compose up command. This saves me from the headache of manual dependency setup and lets me test in conditions very close to real-world usage. The main thing is to remember to manage data volume caches and extract secrets into environment variables.
49 Що таке Page Object Model (POM)? What is the Page Object Model (POM)?
CI/CD, Docker та Архітектурні патерни CI/CD, Docker and architectural patterns junior ▶

Що це — Page Object Model (POM) — це популярний патерн проєктування в автоматизації тестування, який використовується для створення об'єктного сховища елементів вебсторінки. Основна ідея патерну полягає в тому, щоб повністю відокремити бізнес-логіку тестів від технічної структури сторінок додатку, перетворивши кожну вебсторінку або її логічний компонент на окремий клас.

What is it — Page Object Model (POM) is a popular design pattern in test automation used to create an object repository for web page elements. The main idea of the pattern is to completely separate test business logic from the technical structure of application pages by turning each web page or its logical component into a separate class.

Навіщо/коли — Цей підхід працює завдяки інкапсуляції селекторів та методів взаємодії з елементами. Коли вебдодаток змінює свій дизайн або ідентифікатори елементів, тестувальнику не потрібно масово переписувати десятки чи сотні тестів — достатньо оновити локатор лише в одному відповідному класі сторінки. Усі локатори (XPath, CSS) та дії (клік, введення тексту, вибір із випадаючого списку) концентруються в єдиному місці.

Why/when — This approach works through the encapsulation of selectors and interaction methods with elements. When a web application changes its design or element IDs, the tester doesn't need to rewrite dozens or hundreds of tests massively — it is enough to update the locator in just one corresponding page class. All locators (XPath, CSS) and actions (clicking, typing text, selecting from dropdowns) are concentrated in a single place.

Приклад з проєкту — Уявіть проєкт інтернет-магазину, де сценарії авторизації, оформлення замовлення та додавання товару в кошик використовують форму входу. Якщо поле введення email змінить свій ідентифікатор з input-email на user-email, без POM вам довелося б шукати цей селектор у тридцяти різних тестових файлах. Завдяки POM ви робите виправлення рівно один раз у файлі LoginPage.py, і всі залежні тести знову стають працездатними.

Project example — Imagine an e-commerce project where login, checkout, and add-to-cart scenarios all use the login form. If the email input field changes its ID from input-email to user-email, without POM you would have to search for this selector across thirty different test files. Thanks to POM, you make the fix exactly once in the LoginPage.py file, and all dependent tests become operational again.

Підводні камені — POM є обов'язковим стандартом для будь-яких серйозних проєктів з UI-автоматизації, хоча для одноразових мікроскриптів із трьох рядків він буде надлишковим. Поширеною помилкою початківців є спроба додавати перевірки (assertions) всередину класів сторінок. Класи сторінок мають лише описувати інтерфейс і виконувати дії, тоді як самі перевірки очікуваних результатів повинні залишатися виключно у тестових сценаріях.

Pitfalls — POM is an essential standard for any serious UI automation project, although it would be overkill for one-off micro-scripts of three lines. A common beginner mistake is trying to add assertions inside page classes. Page classes should only describe the interface and perform actions, while the checks of expected results must remain exclusively within the test scenarios.

Уточнення на співбесіді — Чи мають Page Object містити assert? Краще ні: сторінка повертає стан, а перевірку робить тест, інакше один об'єкт має дві причини змінюватися.

Interview follow-up — Should Page Objects contain asserts? Better not: the page returns state and the test does the check, otherwise one object has two reasons to change.

Приклад реалізації Page Object Model (Python + Selenium)
Example of Page Object Model implementation (Python + Selenium)
class LoginPage:
    def __init__(self, driver):
        self.driver = driver
        self.email_input = (By.ID, "email")
        self.login_button = (By.ID, "submit-btn")

    def enter_email(self, email):
        self.driver.find_element(*self.email_input).send_keys(email)

    def click_login(self):
        self.driver.find_element(*self.login_button).click()

# Використання у тестах / Usage in tests
def test_successful_login(driver):
    page = LoginPage(driver)
    page.enter_email("user@example.com")
    page.click_login()
    assert "dashboard" in driver.current_url
class LoginPage:
    def __init__(self, driver):
        self.driver = driver
        self.email_input = (By.ID, "email")
        self.login_button = (By.ID, "submit-btn")

    def enter_email(self, email):
        self.driver.find_element(*self.email_input).send_keys(email)

    def click_login(self):
        self.driver.find_element(*self.login_button).click()

# Usage in tests
def test_successful_login(driver):
    page = LoginPage(driver)
    page.enter_email("user@example.com")
    page.click_login()
    assert "dashboard" in driver.current_url
💡 Простими словами: Page Object Model — це як пульт дистанційного керування для вебсторінки. Замість того, щоб щоразу лізти всередину телевізора та перемикати контакти вручну (дублювати локатори в тестах), ви просто тиснете зручні кнопки на пульті (методи сторінки), що робить код чистим і легким у підтримці.
💡 In plain words: Page Object Model is like a remote control for a web page. Instead of climbing inside the television to rewire connections manually every time (duplicating locators in tests), you simply press convenient buttons on the remote (page methods), which keeps your code clean and maintainable.
🗣 Як читати вголос Page Object Model або POM — це базовий патерн в автоматизації тестування, який допомагає тримати структуру коду чистою та спрощує підтримку тестів. Суть у тому, що ми створюємо окремі класи для кожної сторінки сайту, де зберігаємо всі селектори елементів та методи взаємодії з ними. Завдяки цьому логіка самих тестів повністю відділена від розмітки сторінки. Якщо розробники змінюють якийсь ID чи клас кнопки, мені не потрібно правити це у десятках тестів — достатньо змінити локатор лише в одному класі сторінки. Головне правило, про яке я завжди пам'ятаю — не писати перевірки всередині класів сторінок, адже assertions мають залишатися виключно у тестових сценаріях.
🗣 How to say it out loud Page Object Model or POM is a foundational pattern in test automation that helps keep code structure clean and simplifies test maintenance. The essence is that we create separate classes for each page of the site, where we store all element selectors and methods to interact with them. Because of this, the test logic itself is completely separated from the page markup. If developers change an ID or button class, I don't need to fix it across dozens of tests — I just need to update the locator in a single page class. The main rule I always remember is not to write assertions inside page classes, as assertions should remain exclusively within the test scenarios.
50 Яка головна перевага Page Object? What is the main advantage of Page Object?
CI/CD, Docker та Архітектурні патерни CI/CD, Docker and architectural patterns mid ▶

Що це — Головна перевага паттерну Page Object (POM) полягає в забезпеченні високого рівня підтримки (maintainability) та повторного використання коду через повну ізоляцію логіки інтерфейсу від тестової логіки. У цьому підході кожна вебсторінка або її логічний компонент представлені окремим класом, де інкапсульовані локатори елементів та методи взаємодії з ними.

What is it — The main advantage of the Page Object Model (POM) pattern is ensuring high maintainability and code reusability through the complete isolation of UI logic from test logic. In this approach, each web page or its logical component is represented by a separate class that encapsulates element locators and interaction methods.

Навіщо/коли — Це працює завдяки чіткому дотриманню принципу єдиної відповідальності (Single Responsibility Principle). Тестові сценарії містять лише бізнес-логіку перевірок (asserts) та кроки користувача високого рівня, тоді як деталі знаходження DOM-елементів сховані всередині класів сторінок. Коли фронтенд-розробники змінюють ідентифікатор кнопки чи структуру верстки, вам потрібно оновити локатор лише в одному місці — у відповідному класі Page Object, а не правити десятки тестів.

Why/when — This works due to strict adherence to the Single Responsibility Principle. Test scripts contain only validation business logic (asserts) and high-level user steps, while the details of finding DOM elements are hidden inside page classes. When frontend developers change a button's ID or layout structure, you need to update the locator in only one place — the corresponding Page Object class — rather than fixing dozens of tests.

Приклад з проєкту — Уявіть проєкт інтернет-магазину на сотню тестів, де елемент кошика використовується на кожній сторінці. Без Page Object зміна локатора кошика призведе до масового падіння тестів і необхідності редагувати кожен файл вручну, витрачаючи години часу. З використанням POM ви просто оновлюєте метод взаємодії в єдиному компоненті хедера, і всі тести знову зеленіють без додаткових виправлень.

Project example — Imagine an e-commerce project with a hundred tests where the cart element is used on every page. Without Page Object, changing the cart locator leads to massive test failures and the need to edit every file manually, wasting hours of time. With POM, you simply update the interaction method in a single header component, and all tests turn green again without additional fixes.

Підводні камені — Поширеною помилкою серед початківців є перенесення тверджень (assertions) всередину методів Page Object, що перетворює їх на гібриди і руйнує гнучкість тестів. Page Object має повертати стан сторінки, дані або інші об'єкти сторінок, але не перевіряти очікувані результати. Інша крайність — створення «класів-монстрів» (God Pages), що намагаються описувати весь додаток замість розбиття на дрібніші компоненти (модалки, хедера, футери).

Pitfalls — A common mistake among beginners is putting assertions inside Page Object methods, which turns them into hybrids and destroys test flexibility. A Page Object should return page state, data, or other page objects, but not verify expected results. Another extreme is creating "God Pages" that try to describe the entire application instead of breaking it down into smaller components (modals, headers, footers).

Уточнення на співбесіді — Що робити, коли Page Object розростається до сотень рядків? Виносити повторювані частини сторінки в компоненти, наприклад Header чи ProductCard.

Interview follow-up — What do you do when a Page Object grows to hundreds of lines? Extract repeated parts of the page into components, such as Header or ProductCard.

Приклад реалізації Page Object та його використання в тест-кейсі (Playwright / TypeScript)
Example of Page Object implementation and its usage in a test case (Playwright / TypeScript)
export class LoginPage {
  readonly page: Page;
  readonly usernameInput = '#username';
  readonly submitButton = '#login-btn';

  constructor(page: Page) {
    this.page = page;
  }

  async login(user: string, pass: string) {
    await this.page.fill(this.usernameInput, user);
    await this.page.click(this.submitButton);
  }
}

test('Successful login', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.login('test_user', 'secret');
  await expect(page).toHaveURL('/dashboard');
});
💡 Простими словами: Page Object — це як пульт від телевізора. Кнопки перемикання каналів сховані всередині корпусу (це локатори та методи у класі сторінки), а ви як користувач натискаєте лише на саму кнопку пульта у тестах, не думаючи про те, які плати там замикаються всередині.
💡 In plain words: Page Object is like a TV remote. The channel-switching buttons are hidden inside the case (these are locators and methods in the page class), and you as a user just press the remote button in tests without thinking about the internal circuitry.
🗣 Як читати вголосГоловна перевага Page Object — це значне покращення підтримуваності тестів за рахунок чіткого поділу бізнес-логіки тестів та технічної реалізації взаємодії з інтерфейсом. Уявіть ситуацію на проєкті: розробники змінюють верстку чи класи елементів на сторінці логіну. Без Page Object мені довелося б шукати і правити селектори у п'ятдесяти різних тестах. Завдяки паттерну я оновлюю локатор лише в одному місці — у файлі класу LoginPage. Тести стають чистішими, легшими для читання та набагато стійкішими до змін UI.
🗣 How to say it out loudThe main advantage of Page Object is a significant improvement in test maintainability due to a clear separation between test business logic and the technical implementation of UI interactions. Imagine a scenario on a project: developers change the markup or element classes on the login page. Without Page Object, I would have to find and fix selectors in fifty different tests. Thanks to the pattern, I update the locator in just one place — the LoginPage class file. Tests become cleaner, more readable, and much more resilient to UI changes.
51 Що таке Page Factory у Selenium? What is Page Factory in Selenium?
CI/CD, Docker та Архітектурні патерни CI/CD, Docker and architectural patterns mid ▶

Що це — Page Factory — це вбудований у Selenium WebDriver патерн оптимізації для реалізації паттерну Page Object Model (POM), який дозволяє ініціалізувати веб-елементи за допомогою анотацій (наприклад, @FindBy). Він виступає як фабрика за замовчуванням, що зв'язує змінні класу з реальними DOM-елементами на сторінці через механізм рефлексії (Reflection).

What is it — Page Factory is a built-in optimization pattern in Selenium WebDriver used to implement the Page Object Model (POM) design pattern, allowing web elements to be initialized using annotations such as @FindBy. It acts as a default factory that links class variables to actual DOM elements on the page via Java reflection.

Навіщо/коли — Цей механізм працює через виклик методу PageFactory.initElements(), який сканує клас на наявність анотацій та створює динамічні проксі-об'єкти (dynamic proxies) для кожного елемента. Ключовою особливістю є «ліниве» (lazy) завантаження: пошук елемента в DOM відбувається не в момент ініціалізації сторінки, а безпосередньо перед кожним викликом дії над ним (наприклад, кліку чи введення тексту).

Why/when — This mechanism works by calling the PageFactory.initElements() method, which scans the class for annotations and creates dynamic proxy objects for each element. A key feature is lazy loading: the element is not searched in the DOM at the moment of page initialization, but right before each interaction (such as clicking or typing).

Приклад з проєкту — Page Factory доречно використовувати в класичних Java-проєктах на Selenium для зменшення кількості боєйлерплейт-коду (driver.findElement) та централізації локаторів. Проте у сучасній розробці його використання скорочується через певні недоліки: він схильний до частих винятків StaleElementReferenceException при зміні стану сторінки, погано масштабується в умовах надмірно динамічних SPA (Single Page Applications) і, починаючи з Selenium 4, офіційно визнаний застарілим (deprecated) на користь стандартного конструювання через By та Explicit Waits.

Project example — Page Factory is appropriate to use in classic Java Selenium projects to reduce boilerplate code (driver.findElement) and centralize locators. However, in modern development, its usage is declining due to specific drawbacks: it is prone to frequent StaleElementReferenceException errors during state changes, scales poorly in highly dynamic SPAs, and starting from Selenium 4, it has been officially deprecated in favor of standard construction using By locators and Explicit Waits.

Підводні камені — У реальному проєкті ми зіткнулися з цим під час рефакторингу legacy-тестів інтернет-магазину, де локатори дублювалися у тетах. Ми винесли всі елементи каталогу та кошика у відповідні класи Page Factory з анотаціями @FindBy(id = "product-add-to-cart"), що значно покращило читабельність коду та спростило підтримку при зміні ID на фронтенді.

Pitfalls — In a real project, we encountered this during the refactoring of legacy e-commerce tests where locators were duplicated across tests. We moved all catalog and cart elements into respective Page Factory classes annotated with @FindBy(id = "product-add-to-cart"), which significantly improved code readability and simplified maintenance when IDs changed on the frontend.

Уточнююче питання — Найпоширеніша помилка — ігнорування життєвого циклу елементів та відсутність очікувань (Explicit Waits) перед викликом дій, оскільки Page Factory не гарантує автоматичного очікування появи елемента в DOM, а лише його повторний пошук через проксі. Також помилкою є спроба застосувати Page Factory у не-Java мовних привязках (Python, C#), де його реалізація або обмежена, або повністю відсутня.

Follow-up question — The most common mistake is ignoring the element lifecycle and failing to use Explicit Waits before actions, as Page Factory does not guarantee automatic waiting for an element to appear in the DOM, but merely re-searches it via proxy. Another mistake is trying to apply Page Factory in non-Java bindings (Python, C#), where its implementation is either limited or completely absent.

Приклад класу Page Factory на Java
Example of a Page Factory class in Java
public class LoginPage {
    private WebDriver driver;

    @FindBy(id = "username")
    private WebElement usernameInput;

    @FindBy(id = "password")
    private WebElement passwordInput;

    @FindBy(xpath = "//button[@type='submit']")
    private WebElement loginButton;

    public LoginPage(WebDriver driver) {
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }

    public void login(String user, String pass) {
        usernameInput.sendKeys(user);
        passwordInput.sendKeys(pass);
        loginButton.click();
    }
}
💡 Простими словами: Page Factory — це зручний інструмент у Selenium для автоматичного зв'язування змінних у коді з елементами сторінки через анотації на кшталт @FindBy. Він робить класи сторінок чистішими, але зараз вважається застарілим у порівнянні зі звичайними локаторами та явними очікуваннями.
💡 In plain words: Page Factory is a convenient tool in Selenium for automatically binding code variables to page elements using annotations like @FindBy. It keeps page classes cleaner, but is now considered legacy compared to standard locators and explicit waits.
🗣 Як читати вголос Page Factory — це вбудований у Selenium патерн для реалізації Page Object Model, який використовує анотації на кшталт @FindBy для ініціалізації елементів. Він працює через механізм рефлексії та динамічних проксі, здійснюючи пошук елементів "ліниво" перед кожною дією. Я використовував його в legacy-проєктах для зменшення кількості боєйлерплейту та структурування локаторів у класах. Проте на сучасних проєктах я намагаюся уникати Page Factory, оскільки він схильний до StaleElementReferenceException і, починаючи з Selenium 4, офіційно вважається застарілим на користь стандартних локаторів і Explicit Waits.
🗣 How to say it out loud Page Factory is a built-in Selenium pattern for implementing the Page Object Model that uses annotations like @FindBy to initialize elements. It works through reflection and dynamic proxy mechanisms, performing "lazy" element searches right before each action. I used it in legacy projects to reduce boilerplate and structure locators within page classes. However, in modern projects, I tend to avoid Page Factory because it's prone to StaleElementReferenceException, and starting from Selenium 4, it is officially considered deprecated in favor of standard locators and Explicit Waits.
52 Що таке Fluent Page Object? What is a Fluent Page Object?
CI/CD, Docker та Архітектурні патерни CI/CD, Docker and architectural patterns senior ▶

Що це — Fluent Page Object — це розширення класичного патерну Page Object Model (POM), яке використовує концепцію ланцюжкових викликів (method chaining) для написання більш читабельних та компактних автотестів. Основна ідея полягає в тому, що кожен метод взаємодії з елементом сторінки повертає посилання на поточний об'єкт сторінки (this) або на новий об'єкт сторінки, якщо відбувається перехід (наприклад, return new DashboardPage();). Це перетворює набір розрізнених рядків коду на єдиний осмислений ланцюжок дій, схожий на природну мову.

What is it — A Fluent Page Object is an extension of the classic Page Object Model (POM) pattern that leverages method chaining to write more readable and concise automated tests. The core concept is that each interaction method returns a reference to the current page object (this) or a new page object instance if a navigation event occurs (e.g., return new DashboardPage();). This transforms a set of fragmented code lines into a single, cohesive, readable chain of actions resembling natural language.

Навіщо/коли — Механізм роботи базується на патерні проєктування Fluent Interface. У класичному POM після виконання дії метод зазвичай повертає void, через що в тестовому сценарії доводиться щоразу викликати екземпляр сторінки заново (page.typeEmail(); page.typePassword(); page.submit();). У Fluent-підході кожен метод завершується інструкцією повернення контексту. Це дозволяє об'єднувати виклики через крапку, знижуючи кількість дублювання імен змінних і роблячи сценарії схожими на бізнес-вимоги або Behavior-Driven Development (BDD) сценарії.

Why/when — The mechanism relies on the Fluent Interface design pattern. In a classic POM, action methods typically return void, forcing the test script to repeatedly reference the page instance for every single action (page.typeEmail(); page.typePassword(); page.submit();). With the fluent approach, each method ends by returning the current context, allowing actions to be chained together using dot notation. This drastically cuts down on variable name duplication and makes test scripts read like business requirements or Behavior-Driven Development (BDD) specs.

Приклад з проєкту — У реальному проєкті електронної комерції ми використовували Fluent Page Objects для тестування складного процесу оформлення замовлення (Checkout). Тестовий сценарій виглядав надзвичайно чисто: CatalogPage.open().searchItem("Laptop").addToCart().openCart().proceedToCheckout().fillShippingDetails(user).submitPayment().verifyConfirmation();. Такий підхід дозволив нашій QA-команді та Manual-інженерам, які долучалися до написання автоматизації, швидко розуміти логіку тестів без необхідності занурюватися в деталі реалізації локаторів.

Project example — In a real-world e-commerce project, we used Fluent Page Objects to test a complex multi-step checkout flow. The test scenario read exceptionally cleanly: CatalogPage.open().searchItem("Laptop").addToCart().openCart().proceedToCheckout().fillShippingDetails(user).submitPayment().verifyConfirmation();. This approach allowed both our QA team and manual engineers transitioning to automation to quickly grasp test logic without needing to dive into the low-level details of locator implementations.

Підводні камені — Цей патерн варто використовувати у проєктах зі стабільною архітектурою інтерфейсу, де UI-флоу є лінійними та передбачуваними, а читабельність тестів є пріоритетом. Проте його слід уникати або обмежувати на високодинамічних сторінках зі складними розгалуженнями станів, де ланцюжки стають занадто довгими. Занадто розгалужені або довгі ланцюжки ускладнюють відлагодження (debugging), оскільки стек викликів (stack trace) у разі падіння теста може вказувати лише на загальний рядок ланцюжка, ускладнюючи локалізацію конкретної проблемної дії.

Pitfalls — You should use this pattern in projects with stable UI architectures where user flows are linear and predictable, and test readability is a priority. However, it should be avoided or carefully bounded on highly dynamic pages with complex state branching where chains become excessively long. Overly long or branched chains complicate debugging because a test failure stack trace might point only to the entire chained line, making it harder to pinpoint the exact failing action.

Уточнююче питання — Найпоширеніша помилка — створення так званих "мега-ланцюжків" (God chains), що охоплюють усю систему від логіну до закриття сесії в одному нескінченному рядку. Це порушує принцип єдиного обов'язку та робить тест крихким. Також поширеною проблемою є неправильна обробка навігаційних переходів, коли метод повертає this замість нового екземпляра сторінки, що відкривається після кліку, призводячи до помилок часу виконання.

Follow-up question — The most common mistake is creating "God chains" that span the entire application flow from login to session closure in one endless line. This violates the Single Responsibility Principle and makes tests extremely brittle. Another frequent pitfall is mishandling navigation transitions—returning this instead of a new instance of the newly loaded page after a click, which leads to runtime errors.

Приклад реалізації Fluent Page Object (TypeScript)
Fluent Page Object implementation example (TypeScript)
class LoginPage {
    constructor(private page: Page) {}

    async enterUsername(username: string): Promise<this> {
        await this.page.fill('#username', username);
        return this;
    }

    async enterPassword(password: string): Promise<this> {
        await this.page.fill('#password', password);
        return this;
    }

    async clickLogin(): Promise<DashboardPage> {
        await this.page.click('#login-btn');
        return new DashboardPage(this.page);
    }
}
💡 Простими словами: Fluent Page Object — це коли методи сторінки повертають саму сторінку після виконання дії, що дозволяє вибудовувати красиві ланцюжки викликів на кшталт page.login().openDashboard().submitForm(). Це робить код тестів схожим на читання звичайної книги або бізнес-вимоги.
💡 In plain words: A Fluent Page Object is when page methods return the page instance after performing an action, allowing you to string together clean method chains like page.login().openDashboard().submitForm(). This makes your test code read like a book or a business requirement.
🗣 Як читати вголос Fluent Page Object — це еволюція звичайного Page Object Model, де ми використовуємо принцип method chaining для підвищення читабельності тестів. Суть у тому, що кожен метод повертає поточний об'єкт або новий об'єкт сторінки, наприклад return this;. Завдяки цьому сценарії виглядають як послідовний ланцюжок дій через крапку: відкрив, залогінився, перевірив. Це надзвичайно зручно для лінійних сценаріїв на кшталт чекауту в інтернет-магазині, оскільки код стає схожим на опис бізнес-логіки. Проте тут важливо не зловживати і не робити занадто довгі ланцюжки, бо це ускладнює дебагінг, якщо тест падає десь всередині цього єдиного рядка.
🗣 How to say it out loud A Fluent Page Object is an evolution of the standard Page Object Model where we use method chaining to improve test readability. The key idea is that each method returns the current page instance or a new page object, such as return this;. This allows test scenarios to look like a clean, continuous chain of dot-separated actions: open, login, verify. It's especially useful for linear flows like an e-commerce checkout because the code reads just like business logic. However, you have to be careful not to create overly long chains, as that makes debugging much harder when a test fails somewhere deep inside that single statement.
53 Що таке Singleton патерн у контексті WebDriver? What is the Singleton pattern in the WebDriver context?
CI/CD, Docker та Архітектурні патерни CI/CD, Docker and architectural patterns senior ▶

Що це — Патерн Singleton (Одинак) у контексті Selenium WebDriver — це архітектурне рішення, яке гарантує, що під час виконання тестів існує лише один єдиний екземпляр об'єкта WebDriver на весь життєвий цикл потоку чи сесії. Це запобігає випадковому створенню кількох непотрібних екземплярів браузера, економить системні ресурси та централізує керування конфігурацією драйвера (наприклад, налаштуваннями вікна, таймаутами чи аргументами запуску).

What is it — The Singleton pattern in the context of Selenium WebDriver is an architectural design pattern that ensures only a single instance of the WebDriver object exists throughout the test execution lifecycle of a thread or session. This prevents the accidental creation of multiple redundant browser instances, saves system resources, and centralizes driver configuration management (such as window settings, timeouts, or launch arguments).

Навіщо/коли — Механізм реалізації зазвичай базується на приватному конструкторі (private constructor) та статичному методі доступу (наприклад, getInstance()), який перевіряє наявність активного драйвера перед його ініціалізацією. Однак для сучасних проєктів рівня Senior недостатньо класичного Single-thread Singleton, оскільки паралельний запуск тестів у CI/CD (наприклад, через Maven Surefire або PyTest-xdist) призведе до конфліктів і падіння тестів через спільний стан. Тому на практиці застосовують потокобезпечний підхід з використанням ThreadLocal, який ізолює екземпляр драйвера для кожного окремого потоку виконання.

Why/when — The implementation mechanism typically relies on a private constructor and a static access method (e.g., getInstance()) that checks for an existing active driver before initializing a new one. However, for modern Senior-level projects, a classic single-threaded Singleton is insufficient because parallel test execution in CI/CD (via Maven Surefire or PyTest-xdist) will cause race conditions and test failures due to shared state. Therefore, practice dictates using a thread-safe approach via ThreadLocal, which isolates the driver instance for each individual execution thread.

Приклад з проєкту — Уявіть проєкт із тисячами регресійних тестів, які запускаються в Docker-контейнерах на Jenkins-нодах. Без єдиної точки керування драйвером тестові фреймворки можуть хаотично відкривати та закривати браузери або залишати "зомбі-процеси" в пам'яті сервера. Використання Driver Singleton або фабрики драйверів дозволяє централізовано ініціалізувати браузер у базовому класі або хуках (наприклад, @BeforeMethod або setUp), передавати туди динамічні параметри на кшталт HEADLESS=true та гарантовано закривати його в методі tearDown після завершення тесту.

Project example — Imagine a project with thousands of regression tests running in Docker containers on Jenkins nodes. Without a single point of driver management, test frameworks might chaotically open and close browsers or leave "zombie processes" in server memory. Utilizing a Driver Singleton or a driver factory allows for centralized browser initialization in a base class or hooks (e.g., @BeforeMethod or setUp), passing dynamic parameters like HEADLESS=true, and ensuring proper cleanup via tearDown after test completion.

Підводні камені — Найпоширеніша помилка — використання глобальної змінної для драйвера без урахування багатопоточності, що робить паралельний запуск неможливим через перезапис сесій у різних потоках. Інша крайність — створення Singleton на рівні всього тестового процесу без очищення куків та кешу між тестами, що порушує ізоляцію тестів (test isolation) і призводить до ефекту доміно, коли падіння одного тесту ламає наступні. Також важливо пам'ятати про обов'язковий виклик driver.quit(), а не просто driver.close(), щоб уникнути витоків ресурсів операційної системи.

Pitfalls — The most common mistake is using a global variable for the driver without accounting for multithreading, making parallel execution impossible due to session overwriting across threads. Another extreme is creating a Singleton at the entire process level without clearing cookies and cache between tests, which violates test isolation and causes cascading failures where one failing test breaks subsequent ones. It is also critical to remember calling driver.quit() rather than just driver.close() to prevent operating system resource leaks.

Уточнення на співбесіді — Чому класичний Singleton ламає паралельний запуск? Один екземпляр драйвера ділять усі потоки, тому потрібен ThreadLocal або драйвер на кожен тест.

Interview follow-up — Why does a classic Singleton break parallel runs? All threads share one driver instance, so you need a ThreadLocal or a driver per test.

Приклад потокобезпечного Singleton (ThreadLocal WebDriver) на Java
Example of a thread-safe Singleton (ThreadLocal WebDriver) in Java
public class DriverManager {
    private static ThreadLocal<WebDriver> driver = new ThreadLocal<>();

    private DriverManager() {}

    public static WebDriver getDriver() {
        if (driver.get() == null) {
            WebDriver localDriver = new ChromeDriver();
            localDriver.manage().window().maximize();
            driver.set(localDriver);
        }
        return driver.get();
    }

    public static void quitDriver() {
        if (driver.get() != null) {
            driver.get().quit();
            driver.remove(); // Важливо очистити ThreadLocal
        }
    }
}
public class DriverManager {
    private static ThreadLocal<WebDriver> driver = new ThreadLocal<>();

    private DriverManager() {}

    public static WebDriver getDriver() {
        if (driver.get() == null) {
            WebDriver localDriver = new ChromeDriver();
            localDriver.manage().window().maximize();
            driver.set(localDriver);
        }
        return driver.get();
    }

    public static void quitDriver() {
        if (driver.get() != null) {
            driver.get().quit();
            driver.remove(); // Important: clear the ThreadLocal
        }
    }
}
💡 Простими словами: Singleton у WebDriver — це архітектурний захист, який гарантує наявність лише одного екземпляра браузера. У сучасній автоматизації ми використовуємо обгортку ThreadLocal, щоб під час паралельного запуску кожен потік мав свій власний ізольований «одинокий» драйвер і тести не конфліктували між собою.
💡 In plain words: Singleton in WebDriver is an architectural safeguard that guarantees only one browser instance exists. In modern automation, we use a ThreadLocal wrapper so that during parallel runs, each thread has its own isolated "singleton" driver without tests conflicting with one another.
🗣 Як читати вголос У нашому фреймворку ми використовуємо патерн Singleton у поєднанні з ThreadLocal для керування екземпляром WebDriver. Це критично важливо, оскільки дозволяє уникнути конфліктів під час паралельного запуску тестів у CI/CD, ізолюючи браузерну сесію для кожного потоку окремо. Ми реалізували клас-менеджер із приватним конструктором та методом getDriver(), який створює браузер лише за потреби. Крім того, на рівні фікстур ми гарантуємо закриття сесії через driver.quit() та очищення ThreadLocal, що запобігає витокам пам'яті та забезпечує повну ізоляцію тестів.
🗣 How to say it out loud In our framework, we use the Singleton pattern combined with ThreadLocal to manage the WebDriver instance. This is critical because it avoids conflicts during parallel test execution in CI/CD by isolating the browser session for each thread separately. We implemented a manager class with a private constructor and a getDriver() method that instantiates the browser only when needed. Furthermore, at the fixture level, we ensure session termination via driver.quit() and ThreadLocal cleanup, which prevents memory leaks and ensures complete test isolation.
▶ 5 7
54 Навіщо фреймворку ThreadLocal<WebDriver> і як не отримати витік драйверів при паралельному запуску? Why does a framework need ThreadLocal<WebDriver>, and how do you avoid leaking drivers in parallel runs?
Архітектура фреймворку і патерни Framework architecture and patterns senior ▶

Що це — ThreadLocal<WebDriver> — це контейнер, у якому кожен потік бачить власний екземпляр драйвера. Виклик DRIVER.get() з потоку №3 повертає браузер потоку №3 і ніколи не повертає браузер сусіднього потоку.

What it is — ThreadLocal<WebDriver> is a holder in which every thread sees its own driver instance. Calling DRIVER.get() from thread #3 returns thread #3's browser and never the browser of a neighbouring thread.

Навіщо — класичний Singleton зі static WebDriver driver працює, поки тести йдуть послідовно. Щойно вмикаєш паралелізм у JUnit5 чи TestNG, два тести пишуть в одне поле: перший ще клікає, а другий уже перезаписав посилання своїм браузером або викликав quit(). Звідси «рандомні» NoSuchSessionException і кліки не в тому вікні. ThreadLocal дає ізоляцію без передачі драйвера параметром у кожен метод Page Object.

Why — a classic singleton with a static WebDriver driver works while tests run one after another. The moment you enable parallelism in JUnit5 or TestNG, two tests write to the same field: the first is still clicking while the second has already overwritten the reference with its own browser or called quit(). That is where the "random" NoSuchSessionException and clicks in the wrong window come from. ThreadLocal gives isolation without passing the driver as a parameter into every Page Object method.

Приклад — DriverManager створює драйвер ліниво в getDriver(), а в @AfterEach викликає quitDriver(), який робить і quit(), і remove().

Example — a DriverManager creates the driver lazily in getDriver(), and @AfterEach calls quitDriver(), which does both quit() and remove().

Підводні камені — пули потоків перевикористовують потоки. Якщо забути remove(), наступний тест на тому ж потоці отримає мертву сесію після quit(), а класлоадер тримає посилання — це вже витік пам'яті. Друга пастка — асинхронний код: у CompletableFuture інший потік, і там get() поверне null.

Pitfalls — thread pools reuse threads. Forget remove() and the next test on the same thread gets a dead session after quit(), while the value keeps a reference alive — a real memory leak. The second trap is async code: a CompletableFuture runs on another thread, where get() returns null.

Уточнення на співбесіді — «А в Selenide чи Playwright це теж треба?» Ні: Selenide сам тримає драйвер на потік, а Playwright Test дає новий контекст кожному тесту через фікстури.

Interview follow-up — "Do you need this in Selenide or Playwright?" No: Selenide already keeps one driver per thread, and Playwright Test hands each test a fresh context through fixtures.

Java — DriverManager на ThreadLocal
Java — a ThreadLocal DriverManager
public final class DriverManager {
  // кожен потік JUnit має власний слот
  private static final ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>();

  public static WebDriver getDriver() {
    if (DRIVER.get() == null) {
      DRIVER.set(DriverFactory.create()); // лінива ініціалізація
    }
    return DRIVER.get();
  }

  public static void quitDriver() {
    WebDriver d = DRIVER.get();
    if (d != null) {
      try { d.quit(); }
      finally { DRIVER.remove(); } // без цього потік з пулу отримає мертву сесію
    }
  }
}

class BaseUiTest {
  @AfterEach
  void tearDown() { DriverManager.quitDriver(); }
}
public final class DriverManager {
  // every JUnit thread gets its own slot
  private static final ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>();

  public static WebDriver getDriver() {
    if (DRIVER.get() == null) {
      DRIVER.set(DriverFactory.create()); // lazy initialisation
    }
    return DRIVER.get();
  }

  public static void quitDriver() {
    WebDriver d = DRIVER.get();
    if (d != null) {
      try { d.quit(); }
      finally { DRIVER.remove(); } // otherwise a pooled thread inherits a dead session
    }
  }
}

class BaseUiTest {
  @AfterEach
  void tearDown() { DriverManager.quitDriver(); }
}
Що в коді: set/get працюють лише з поточним потоком, а remove() у блоці finally гарантує очищення слота навіть тоді, коли quit() кинув виняток через уже закритий браузер.
What the code shows: set/get only touch the current thread, and remove() inside finally guarantees the slot is cleared even when quit() throws because the browser is already gone.
💡 Простими словами: ThreadLocal — це шафка з іменем потоку на дверцятах. Поклав браузер — забери його перед тим, як шафку отримає наступний «орендар», інакше він відкриє чужі речі.
💡 In plain words: ThreadLocal is a locker with the thread's name on the door. Put a browser in, and take it out before the next "tenant" gets the locker, or they will open someone else's stuff.
🗣 Як читати вголос У нас спершу був статичний драйвер, і все було добре, поки ми не ввімкнули паралельний запуск на чотири потоки. Почалися падіння з закритою сесією, які неможливо відтворити локально. Я переніс драйвер у ThreadLocal, і важливий нюанс, про який часто забувають, — викликати remove після quit, бо Surefire і JUnit перевикористовують потоки. Після цього падіння зникли. Зараз, якщо стек дозволяє, я б узагалі взяв Selenide або Playwright, де ця ізоляція вже вбудована.
🗣 How to say it out loud We started with a static driver and it was fine until we switched on parallel runs with four threads. Then we got closed-session failures that nobody could reproduce locally. I moved the driver into a ThreadLocal, and the detail people often forget is calling remove after quit, because Surefire and JUnit reuse threads. After that the failures disappeared. Today, if the stack allows it, I would simply take Selenide or Playwright, where that isolation is already built in.
55 Як патерн Builder робить тестові дані читабельними і де він шкодить? How does the Builder pattern make test data readable, and where does it hurt?
Архітектура фреймворку і патерни Framework architecture and patterns middle ▶

Що це — Builder збирає об'єкт покроково: aUser().withRole(ADMIN).build(). Для тестових даних він має валідні значення за замовчуванням, тож тест перевизначає лише те, що впливає на перевірку.

What it is — a Builder assembles an object step by step: aUser().withRole(ADMIN).build(). For test data it carries valid defaults, so a test overrides only the fields that affect the check.

Навіщо — конструктор new User("Ann", "a@x.io", 30, "UA", true, null, ADMIN) не читається: неясно, яке поле важливе. З білдером тест про заблокованого користувача містить лише .blocked(), а решта полів сховані й валідні. Коли в моделі з'являється нове обов'язкове поле, ти правиш один дефолт у білдері, а не сотню тестів.

Why — a constructor like new User("Ann", "a@x.io", 30, "UA", true, null, ADMIN) is unreadable: you cannot tell which field matters. With a builder, a test about a blocked user contains only .blocked(), and every other field is hidden and valid. When the model gains a new mandatory field, you fix one default in the builder instead of a hundred tests.

Приклад — у Java зручно взяти Lombok @Builder(toBuilder = true) або написати Test Data Builder вручну; у TypeScript те саме робить функція buildUser(overrides) зі spread-оператором.

Example — in Java you can use Lombok @Builder(toBuilder = true) or hand-write a Test Data Builder; in TypeScript a buildUser(overrides) function with the spread operator does the same job.

Підводні камені — випадкові дефолти (Faker) без логування seed роблять падіння невідтворюваним. Небезпечний і один статичний білдер, спільний для тестів: хтось викликав withEmail, і наступний тест отримав чужий email. Третя проблема — білдер, що мовчки підставляє критичне поле, від якого залежить assert: тоді читач тесту не бачить причини результату.

Pitfalls — random defaults (Faker) without a logged seed make failures irreproducible. A single static builder shared by tests is dangerous too: someone called withEmail, and the next test got a stranger's email. The third problem is a builder that silently fills a field the assert depends on, so the reader cannot see why the result happened.

Уточнення на співбесіді — «Чим Builder відрізняється від Object Mother?» Mother дає готові персонажі (Users.admin()), Builder дозволяє їх тонко налаштувати; часто Mother повертає вже заповнений білдер.

Interview follow-up — "How does a Builder differ from an Object Mother?" A Mother hands out ready personas (Users.admin()), a Builder lets you fine-tune them; often the Mother returns a pre-filled builder.

Java — Test Data Builder і тест, що показує лише важливе поле
Java — a Test Data Builder and a test that shows only the relevant field
public class UserBuilder {
  private String email = "user-" + UUID.randomUUID() + "@test.io";
  private Role role = Role.CUSTOMER;  // валідні дефолти
  private boolean blocked = false;

  public static UserBuilder aUser() { return new UserBuilder(); }
  public UserBuilder withRole(Role r) { role = r; return this; }
  public UserBuilder blocked() { blocked = true; return this; }
  public User build() { return new User(email, role, blocked); }
}

@Test
void blockedUserCannotLogIn() {
  User user = api.create(aUser().blocked().build()); // видно лише суть тесту
  assertThat(auth.login(user).statusCode()).isEqualTo(403);
}
public class UserBuilder {
  private String email = "user-" + UUID.randomUUID() + "@test.io";
  private Role role = Role.CUSTOMER;  // valid defaults
  private boolean blocked = false;

  public static UserBuilder aUser() { return new UserBuilder(); }
  public UserBuilder withRole(Role r) { role = r; return this; }
  public UserBuilder blocked() { blocked = true; return this; }
  public User build() { return new User(email, role, blocked); }
}

@Test
void blockedUserCannotLogIn() {
  User user = api.create(aUser().blocked().build()); // only the point of the test is visible
  assertThat(auth.login(user).statusCode()).isEqualTo(403);
}
Що в коді: кожен with… повертає this, тому виклики ланцюжаться; aUser() — статична фабрика, що створює новий білдер на кожен тест, а унікальний email одразу знімає конфлікти даних.
What the code shows: every with… returns this, so calls chain; aUser() is a static factory that creates a fresh builder per test, and the unique email removes data clashes up front.
💡 Простими словами: якщо поле впливає на результат тесту — воно має бути видно в тілі тесту. Все інше ховай у дефолти білдера.
💡 In plain words: if a field affects the outcome of the test, it must be visible in the test body. Everything else goes into the builder's defaults.
🗣 Як читати вголос Я використовую білдери, коли модель має більше трьох-чотирьох полів. Правило просте: дефолти завжди валідні, а в тесті я пишу лише те, що перевіряю, — наприклад, роль адміністратора. Коли бекенд додав обов'язкове поле телефону, ми поправили один рядок у білдері, і жоден тест не довелося чіпати. Єдине, за чим стежу, — щоб білдер не був спільним статичним об'єктом і щоб випадкові значення логувалися.
🗣 How to say it out loud I reach for builders once a model has more than three or four fields. The rule is simple: defaults are always valid, and in the test I write only what I am checking, say the admin role. When the backend added a mandatory phone field, we changed one line in the builder and didn't touch a single test. The only things I watch are that the builder is never a shared static object and that random values get logged.
56 Як DriverFactory обирає браузер або клієнт з конфігурації і коли фабрика стає проблемою? How does a DriverFactory pick a browser or client from configuration, and when does the factory become a problem?
Архітектура фреймворку і патерни Framework architecture and patterns middle ▶

Що це — Factory — це одне місце, яке знає, як створити об'єкт потрібного типу. Тест просить «дай драйвер», а фабрика читає browser=firefox або remote=true і повертає FirefoxDriver чи RemoteWebDriver на Grid.

What it is — a Factory is the one place that knows how to create an object of the right type. The test asks "give me a driver", and the factory reads browser=firefox or remote=true and returns a FirefoxDriver or a RemoteWebDriver pointing at a Grid.

Навіщо — без фабрики new ChromeDriver() розкиданий по базових класах, і щоб прогнати регресію у Firefox у CI, треба правити код. З фабрикою досить параметра -Dbrowser=firefox. Те саме працює для API: ApiClientFactory повертає клієнт з потрібним baseURI й автентифікацією для dev чи stage.

Why — without a factory, new ChromeDriver() is scattered across base classes, and running regression in Firefox on CI means editing code. With a factory a -Dbrowser=firefox parameter is enough. The same works for APIs: an ApiClientFactory returns a client with the right baseURI and authentication for dev or stage.

Приклад — switch-вираз Java 17 перетворює рядок конфігу на enum і створює драйвер з потрібними options; невідоме значення одразу кидає виняток із зрозумілим повідомленням, а не падає десь у середині тесту.

Example — a Java 17 switch expression turns the config string into an enum and creates the driver with the right options; an unknown value fails immediately with a clear message instead of crashing somewhere in the middle of a test.

Підводні камені — фабрика обростає прапорцями (мобільна емуляція, проксі, розширення, розмір вікна) і перетворюється на god object на 400 рядків. Краще винести збирання options в окремі методи або класи на кожен браузер. Друга помилка — читати System.getProperty у п'яти місцях замість одного конфіг-класу: тоді значення по замовчуванню розходяться.

Pitfalls — the factory accretes flags (mobile emulation, proxy, extensions, window size) and grows into a 400-line god object. It is better to move options assembly into separate methods or per-browser classes. The second mistake is reading System.getProperty in five places instead of one config class, so the defaults drift apart.

Уточнення на співбесіді — «Factory чи Strategy?» Фабрика відповідає на питання «який об'єкт створити», стратегія — «яку поведінку виконати»; фабрика часто саме й повертає обрану стратегію.

Interview follow-up — "Factory or Strategy?" A factory answers "which object do I create", a strategy answers "which behaviour do I run"; quite often the factory is exactly what returns the chosen strategy.

Java — фабрика драйверів на switch-виразі
Java — a driver factory with a switch expression
public final class DriverFactory {
  public static WebDriver create() {
    Browser browser = Browser.valueOf(Config.browser().toUpperCase());
    if (Config.remoteUrl() != null) {       // Grid або Selenoid у CI
      return new RemoteWebDriver(Config.remoteUrl(), options(browser));
    }
    return switch (browser) {
      case CHROME  -> new ChromeDriver((ChromeOptions) options(browser));
      case FIREFOX -> new FirefoxDriver((FirefoxOptions) options(browser));
    };
  }

  // збирання options винесене окремо, щоб фабрика не розрослася
  private static Capabilities options(Browser b) {
    return switch (b) {
      case CHROME  -> new ChromeOptions().addArguments("--headless=new");
      case FIREFOX -> new FirefoxOptions().addArguments("-headless");
    };
  }
}
public final class DriverFactory {
  public static WebDriver create() {
    Browser browser = Browser.valueOf(Config.browser().toUpperCase());
    if (Config.remoteUrl() != null) {       // Grid or Selenoid on CI
      return new RemoteWebDriver(Config.remoteUrl(), options(browser));
    }
    return switch (browser) {
      case CHROME  -> new ChromeDriver((ChromeOptions) options(browser));
      case FIREFOX -> new FirefoxDriver((FirefoxOptions) options(browser));
    };
  }

  // options assembly lives apart so the factory does not bloat
  private static Capabilities options(Browser b) {
    return switch (b) {
      case CHROME  -> new ChromeOptions().addArguments("--headless=new");
      case FIREFOX -> new FirefoxOptions().addArguments("-headless");
    };
  }
}
Що в коді: switch по enum без default компілятор перевіряє на повноту — додаси EDGE в enum, і код не збереться, доки не опишеш новий випадок. Browser.valueOf кине IllegalArgumentException на опечатку в конфігу ще до старту браузера.
What the code shows: a switch over an enum without default is checked for exhaustiveness — add EDGE to the enum and the code will not compile until you handle it. Browser.valueOf throws IllegalArgumentException on a config typo before any browser starts.
💡 Простими словами: слово new для драйвера чи HTTP-клієнта має зустрічатися у фреймворку рівно в одному класі. Якщо знаходиш його в тестах — фабрики фактично немає.
💡 In plain words: the word new for a driver or HTTP client should appear in exactly one class of the framework. If you find it in tests, you effectively have no factory.
🗣 Як читати вголос У моєму фреймворку тести взагалі не знають, який браузер запущено. Є DriverFactory, яка читає конфіг: локально це Chrome, у нічному CI — Firefox і Grid через remote URL. Коли з'явилася вимога ганяти ще й Edge, я додав одне значення в enum і один case, тести не змінилися. Єдине — я стежу, щоб фабрика не перетворилася на смітник прапорців, тому налаштування опцій винесені окремо.
🗣 How to say it out loud In my framework the tests have no idea which browser is running. There is a DriverFactory that reads config: locally it's Chrome, on the nightly CI it's Firefox on a Grid through a remote URL. When we were asked to run Edge as well, I added one enum value and one case, and no test changed. The one thing I watch is that the factory doesn't become a dumping ground for flags, so building the options lives separately.
57 Де патерн Strategy реально окупається в тестовому фреймворку, наприклад для способів логіну? Where does the Strategy pattern really pay off in a test framework, for example for login methods?
Архітектура фреймворку і патерни Framework architecture and patterns senior ▶

Що це — Strategy виносить змінну поведінку за інтерфейс і дає підставити потрібну реалізацію під час виконання. Класичний тестовий приклад — LoginStrategy з трьома реалізаціями: логін через форму, через API-токен і через підкладання готової cookie.

What it is — Strategy moves varying behaviour behind an interface and lets you plug in the right implementation at runtime. The classic test example is a LoginStrategy with three implementations: log in through the form, via an API token, or by injecting a ready cookie.

Коли окупається — логін через UI потрібен у двох-трьох тестах, які перевіряють саму форму. Решта 300 тестів мають лише опинитися «залогіненими», і UI-логін додає їм 5–8 секунд і ще одну точку падіння. Зі стратегією тест кошика викликає loginAs(user), а фреймворк за конфігом чи анотацією обирає API-варіант; на середовищі без API для токенів той самий тест працює через форму без змін.

When it pays off — UI login is needed in two or three tests that check the form itself. The other 300 tests just need to end up "logged in", and a UI login costs them 5–8 seconds and one more point of failure. With a strategy, the cart test calls loginAs(user), and the framework picks the API variant from config or an annotation; on an environment without a token API the same test goes through the form unchanged.

Приклад — ще доречні місця: різні способи генерації даних (API чи SQL), платіжні провайдери в тестовому режимі, валідація відповіді для v1 і v2 API.

Example — other good spots: different ways to seed data (API vs SQL), payment providers in sandbox mode, response validation for v1 and v2 of an API.

Підводні камені — інтерфейс з однією реалізацією «на майбутнє» — це лише зайвий рівень абстракції; я вводжу Strategy, коли є мінімум два реальні варіанти. Інший трейдоф: API-логін не перевіряє фронтову частину автентифікації, тож потрібен хоча б один тест, що проходить повний UI-шлях.

Pitfalls — an interface with a single implementation "for the future" is just an extra layer of indirection; I introduce Strategy when there are at least two real variants. The other trade-off: API login skips the frontend part of authentication, so at least one test must still walk the full UI path.

Уточнення на співбесіді — «Чим це краще за if/else?» Новий спосіб логіну — це новий клас, а не ще одна гілка в методі, яку зачіпають усі; кожну реалізацію можна перевірити окремо.

Interview follow-up — "Why is it better than if/else?" A new login method is a new class, not another branch in a method everyone touches, and each implementation can be verified on its own.

Java — LoginStrategy з UI- і API-реалізацією
Java — LoginStrategy with UI and API implementations
public interface LoginStrategy { void login(User user); }

class UiLogin implements LoginStrategy {
  public void login(User u) { new LoginPage().open().signIn(u.email(), u.password()); }
}

class ApiLogin implements LoginStrategy {
  public void login(User u) {
    String token = authClient.token(u);           // ~200 мс замість ~6 с UI
    driver().get(Config.baseUrl());                 // cookie ставиться лише на відкритий домен
    driver().manage().addCookie(new Cookie("session", token));
  }
}

// тест не знає, як саме відбувся логін
@Test
void addsItemToCart() {
  Session.loginStrategy().login(aUser().build());
  cart.add("SKU-42");
  assertThat(cart.count()).isEqualTo(1);
}
public interface LoginStrategy { void login(User user); }

class UiLogin implements LoginStrategy {
  public void login(User u) { new LoginPage().open().signIn(u.email(), u.password()); }
}

class ApiLogin implements LoginStrategy {
  public void login(User u) {
    String token = authClient.token(u);           // ~200 ms instead of ~6 s of UI
    driver().get(Config.baseUrl());                 // a cookie can only be set on an open domain
    driver().manage().addCookie(new Cookie("session", token));
  }
}

// the test does not know how the login happened
@Test
void addsItemToCart() {
  Session.loginStrategy().login(aUser().build());
  cart.add("SKU-42");
  assertThat(cart.count()).isEqualTo(1);
}
Що в коді: Session.loginStrategy() повертає реалізацію за конфігом; ApiLogin спершу відкриває домен, бо Selenium не дозволяє ставити cookie на сторінці about:blank.
What the code shows: Session.loginStrategy() returns the implementation chosen by config; ApiLogin opens the domain first because Selenium will not set a cookie on about:blank.
💡 Простими словами: Strategy — це «що зробити» з кількома взаємозамінними «як». Поки «як» лише одне, інтерфейс не потрібен.
💡 In plain words: Strategy is one "what to do" with several interchangeable "hows". While there is only one "how", you do not need the interface.
🗣 Як читати вголос Найкорисніше застосування Strategy, яке я бачив, — це логін. У нас UI-регресія займала сорок хвилин, і чверть часу йшла на заповнення форми входу в кожному тесті. Ми зробили інтерфейс LoginStrategy, і за замовчуванням тести логіняться через API й cookie, а кілька тестів форми явно використовують UI-варіант. Прогін скоротився приблизно на десять хвилин. Але я не створюю стратегії заздалегідь — лише коли є два реальні способи.
🗣 How to say it out loud The most useful Strategy I have seen is login. Our UI regression took forty minutes, and a quarter of that went on filling the sign-in form in every test. We introduced a LoginStrategy interface: by default tests log in through the API and a cookie, and a handful of form tests explicitly use the UI variant. The run got about ten minutes shorter. But I don't create strategies up front — only when there are two real ways to do the thing.
58 На які шари ви ділите тестовий фреймворк і що в якому шарі заборонено? Which layers do you split a test framework into, and what is forbidden in each layer?
Архітектура фреймворку і патерни Framework architecture and patterns senior ▶

Що це — типовий розподіл: tests (сценарії й асерти) → steps або бізнес-дії (checkout.payByCard()) → pages/components і API clients (як взаємодіяти з системою) → core (драйвер, конфіг, HTTP, логування, звіти). Залежності йдуть лише згори донизу.

What it is — a typical split: tests (scenarios and asserts) → steps or business actions (checkout.payByCard()) → pages/components and API clients (how to interact with the system) → core (driver, config, HTTP, logging, reporting). Dependencies point only downwards.

Навіщо — кожен шар має одну причину змінюватися. Змінився локатор — правка в page. Змінився ендпоінт — у клієнті. Змінився бізнес-флоу — у steps. Перейшли з Selenium на Playwright — переписується core і pages, а тести лишаються. Це і є критерій вдалої архітектури: вартість типової зміни локальна.

Why — each layer has one reason to change. A locator changes — edit the page. An endpoint changes — edit the client. A business flow changes — edit the steps. Moving from Selenium to Playwright rewrites core and pages while tests stay. That is the test of a good architecture: the cost of a typical change stays local.

Приклад — правила, які я закріплюю в рев'ю або ArchUnit: тести не імпортують org.openqa.selenium; page objects не містять assertThat, а повертають стан; API-клієнти не знають про UI; core не залежить ні від чого вище.

Example — rules I enforce in review or with ArchUnit: tests do not import org.openqa.selenium; page objects contain no assertThat and return state instead; API clients know nothing about the UI; core depends on nothing above it.

Підводні камені — асерти в page object роблять його непридатним для негативних сценаріїв. Прямі виклики driver.findElement у тестах обходять усі очікування фреймворку. І протилежна крайність — п'ять шарів для двадцяти тестів: steps, що лише проксюють page, — це церемонія без користі.

Pitfalls — asserts inside a page object make it useless for negative scenarios. Direct driver.findElement calls in tests bypass every wait the framework has. And the opposite extreme is five layers for twenty tests: steps that just proxy a page are ceremony without value.

Уточнення на співбесіді — «Де живуть тестові дані?» Окремий модуль data: білдери та фікстури, які використовують і API-клієнти для підготовки стану, і тести.

Interview follow-up — "Where does test data live?" In a separate data module: builders and fixtures used both by API clients to set up state and by tests.

Структура пакетів і правило ArchUnit
Package layout and an ArchUnit rule
// src/test/java/com/shop/
//   tests/       CheckoutTest       — сценарії та асерти
//   steps/       CheckoutSteps      — бізнес-дії з кількох сторінок
//   pages/       CartPage, Header   — локатори й дії на UI
//   api/         OrdersClient       — HTTP-виклики
//   data/        UserBuilder        — тестові дані
//   core/        DriverManager, Config, AllureListener

@AnalyzeClasses(packages = "com.shop")
class LayerRulesTest {
  @ArchTest
  static final ArchRule testsDoNotTouchSelenium =
      noClasses().that().resideInAPackage("..tests..")
          .should().dependOnClassesThat().resideInAPackage("org.openqa.selenium..");

  @ArchTest
  static final ArchRule pagesHaveNoAsserts =
      noClasses().that().resideInAPackage("..pages..")
          .should().dependOnClassesThat().resideInAPackage("org.assertj..");
}
// src/test/java/com/shop/
//   tests/       CheckoutTest       — scenarios and asserts
//   steps/       CheckoutSteps      — business actions spanning pages
//   pages/       CartPage, Header   — UI locators and actions
//   api/         OrdersClient       — HTTP calls
//   data/        UserBuilder        — test data
//   core/        DriverManager, Config, AllureListener

@AnalyzeClasses(packages = "com.shop")
class LayerRulesTest {
  @ArchTest
  static final ArchRule testsDoNotTouchSelenium =
      noClasses().that().resideInAPackage("..tests..")
          .should().dependOnClassesThat().resideInAPackage("org.openqa.selenium..");

  @ArchTest
  static final ArchRule pagesHaveNoAsserts =
      noClasses().that().resideInAPackage("..pages..")
          .should().dependOnClassesThat().resideInAPackage("org.assertj..");
}
Що в коді: ArchUnit перевіряє залежності між пакетами як звичайний JUnit-тест, тож порушення шарів ламає збірку, а не залишається усною домовленістю.
What the code shows: ArchUnit checks package dependencies as an ordinary JUnit test, so a layering violation breaks the build instead of staying a verbal agreement.
💡 Простими словами: перевір архітектуру уявним переходом на інший інструмент: якщо довелося б переписувати тести, а не лише нижні шари, межі проведені неправильно.
💡 In plain words: test your architecture with an imaginary tool migration: if you would have to rewrite the tests and not just the lower layers, the boundaries are in the wrong place.
🗣 Як читати вголос Я думаю про фреймворк як про чотири поверхи: тести, бізнес-кроки, сторінки та API-клієнти, і ядро з драйвером і конфігом. Ходити можна лише вниз. На попередньому проєкті ми саме так переїхали з Selenium на Playwright за кілька спринтів, і тести майже не змінилися. А щоб правила не розмивалися, я додав ArchUnit — він падає, якщо хтось імпортує Selenium прямо в тест.
🗣 How to say it out loud I think of a framework as four floors: tests, business steps, pages and API clients, and a core with the driver and config. You may only go downstairs. On my previous project that is exactly how we moved from Selenium to Playwright in a few sprints, and the tests barely changed. To keep the rules from eroding, I added ArchUnit, which fails when someone imports Selenium straight into a test.
59 Як організувати конфігурацію середовищ і секрети в тестовому фреймворку? How do you organise environment configuration and secrets in a test framework?
Архітектура фреймворку і патерни Framework architecture and patterns middle ▶

Що це — один типізований конфіг-клас, що збирає значення за пріоритетом: системна властивість -Dkey → змінна оточення → файл середовища (dev.properties, stage.properties) → дефолт. Середовище обирається одним параметром, наприклад -Denv=stage.

What it is — one typed config class that resolves values by priority: system property -Dkey → environment variable → environment file (dev.properties, stage.properties) → default. The environment is chosen by a single parameter, for example -Denv=stage.

Навіщо — той самий набір тестів має бігати локально, у PR-пайплайні на dev і вночі на stage без правок коду. Бібліотека Owner для Java дає інтерфейс з анотаціями @Key і @DefaultValue та підставляє змінні; у TypeScript те саме роблять dotenv і функція, що читає process.env та перевіряє обов'язкові ключі.

Why — the same suite has to run locally, in the PR pipeline against dev and nightly against stage without code edits. The Owner library for Java gives an interface with @Key and @DefaultValue annotations and resolves variables; in TypeScript dotenv plus a function that reads process.env and checks required keys does the same.

Приклад — URL і таймаути лежать у репозиторії, а паролі й API-ключі — лише в сховищі секретів CI (GitHub Actions secrets, Vault) і потрапляють у тести як змінні оточення; локально — з файлу .env, який внесено в .gitignore.

Example — URLs and timeouts live in the repository, while passwords and API keys live only in the CI secret store (GitHub Actions secrets, Vault) and reach the tests as environment variables; locally they come from a .env file listed in .gitignore.

Підводні камені — тихий дефолт для URL: забули змінну в CI, і нічний прогін пішов у production. Тому для критичних ключів дефолту немає — фреймворк падає на старті з назвою відсутньої змінної. Ще одна пастка — секрет у логах: не друкуй увесь конфіг і не додавай токени в Allure-вкладення.

Pitfalls — a silent default for the URL: someone forgot the variable in CI, and the nightly run went to production. So critical keys have no default — the framework fails at startup naming the missing variable. Another trap is a secret in the logs: never print the whole config and never put tokens into Allure attachments.

Уточнення на співбесіді — «Як перевірити, що секрет витік?» GitHub маскує зареєстровані секрети в логах, але не в артефактах; у репозиторії допомагає gitleaks у pre-commit.

Interview follow-up — "How do you catch a leaked secret?" GitHub masks registered secrets in logs but not in artifacts; in the repository, gitleaks in pre-commit helps.

Java (Owner) — типізований конфіг із вибором середовища
Java (Owner) — typed config with environment selection
@Config.LoadPolicy(Config.LoadType.MERGE)
@Config.Sources({
    "system:properties",                    // -DbaseUrl=... має найвищий пріоритет
    "system:env",                           // змінні оточення з CI
    "classpath:env/${env}.properties"        // dev / stage файл
})
public interface TestConfig extends Config {
  @Key("baseUrl")                          // без дефолту — впаде, якщо не задано
  String baseUrl();

  @Key("timeout.seconds") @DefaultValue("10")
  int timeoutSeconds();

  @Key("ADMIN_PASSWORD")                   // лише з секретів CI або .env
  String adminPassword();
}

TestConfig cfg = ConfigFactory.create(TestConfig.class);
Objects.requireNonNull(cfg.baseUrl(), "baseUrl не задано: передайте -Denv або BASE_URL");
@Config.LoadPolicy(Config.LoadType.MERGE)
@Config.Sources({
    "system:properties",                    // -DbaseUrl=... wins
    "system:env",                           // environment variables from CI
    "classpath:env/${env}.properties"        // dev / stage file
})
public interface TestConfig extends Config {
  @Key("baseUrl")                          // no default — fails when missing
  String baseUrl();

  @Key("timeout.seconds") @DefaultValue("10")
  int timeoutSeconds();

  @Key("ADMIN_PASSWORD")                   // only from CI secrets or .env
  String adminPassword();
}

TestConfig cfg = ConfigFactory.create(TestConfig.class);
Objects.requireNonNull(cfg.baseUrl(), "baseUrl is not set: pass -Denv or BASE_URL");
Що в коді: LoadType.MERGE проходить усі джерела по черзі й бере перше знайдене значення; ${env} у шляху Owner підставляє із системної властивості, тому -Denv=stage перемикає файл.
What the code shows: LoadType.MERGE walks the sources in order and takes the first value found; Owner expands ${env} in the path from a system property, so -Denv=stage switches the file.
💡 Простими словами: конфіг у репозиторії, секрети — ніколи. А для URL середовища краще впасти на старті, ніж мовчки піти не туди.
💡 In plain words: config goes in the repo, secrets never do. And for the environment URL it is better to fail at startup than to silently go somewhere else.
🗣 Як читати вголос У нас один інтерфейс конфігу на бібліотеці Owner, і середовище перемикається прапорцем env. Порядок пріоритетів — системні властивості, потім змінні оточення, потім файл. Паролі живуть лише в секретах GitHub Actions, локально — у файлі .env, який не комітиться. Одного разу нічні тести через відсутню змінну пішли на прод, тож відтоді в URL немає значення за замовчуванням: фреймворк падає і чітко пише, чого бракує.
🗣 How to say it out loud We have one config interface built on the Owner library, and the environment is switched by an env flag. The priority order is system properties, then environment variables, then the file. Passwords live only in GitHub Actions secrets; locally they're in a .env file that never gets committed. Once, a missing variable sent our nightly tests to production, so since then the URL has no default: the framework fails and says clearly what is missing.
60 Як написати власне розширення JUnit5, яке готує користувача і збирає докази при падінні? How do you write a custom JUnit5 extension that prepares a user and collects evidence on failure?
Архітектура фреймворку і патерни Framework architecture and patterns senior ▶

Що це — розширення JUnit5 — це клас, що реалізує інтерфейси точок розширення: BeforeEachCallback, AfterEachCallback, ParameterResolver, TestWatcher тощо. Його підключають через @ExtendWith або власну анотацію, яка вже містить @ExtendWith.

What it is — a JUnit5 extension is a class that implements extension-point interfaces: BeforeEachCallback, AfterEachCallback, ParameterResolver, TestWatcher and so on. You attach it with @ExtendWith or with your own annotation that already carries @ExtendWith.

Навіщо — базовий клас із @BeforeEach змушує всі тести успадковувати все підряд, а розширення компонуються: тест просить параметр User user, і ParameterResolver створює його через API, а після тесту видаляє. TestWatcher.testFailed викликається лише при падінні, тож скріншот і логи збираються не для кожного зеленого тесту.

Why — a base class with @BeforeEach forces every test to inherit everything, whereas extensions compose: a test asks for a User user parameter, and a ParameterResolver creates it via the API and deletes it after the test. TestWatcher.testFailed fires only on failure, so screenshots and logs are not collected for every green test.

Приклад — стан між колбеками зберігається в ExtensionContext.Store з власним Namespace, а не в полях розширення: один екземпляр розширення може обслуговувати паралельні тести.

Example — state between callbacks is kept in ExtensionContext.Store under your own Namespace, not in extension fields: one extension instance may serve parallel tests.

Підводні камені — порядок: testFailed викликається вже після @AfterEach, тож якщо драйвер закрито в @AfterEach, скріншот робити пізно; тоді знімай його в AfterEachCallback через context.getExecutionException() або використовуй TestExecutionExceptionHandler. Поля в розширенні замість Store дають гонки при паралельному запуску.

Pitfalls — ordering: testFailed runs after @AfterEach, so if the driver is closed there, it is too late for a screenshot; take it in an AfterEachCallback via context.getExecutionException() or use a TestExecutionExceptionHandler. Fields in the extension instead of the Store cause races in parallel runs.

Уточнення на співбесіді — «А як у Playwright?» test.extend оголошує фікстуру з кодом до і після use(); це той самий принцип, лише декларативніше.

Interview follow-up — "And in Playwright?" test.extend declares a fixture with code before and after use(); same principle, just more declarative.

Java — розширення, що створює користувача й прибирає за ним
Java — an extension that creates a user and cleans up after it
public class UserExtension implements ParameterResolver, AfterEachCallback {
  private static final Namespace NS = Namespace.create(UserExtension.class);

  public boolean supportsParameter(ParameterContext p, ExtensionContext c) {
    return p.getParameter().getType() == User.class;
  }

  public Object resolveParameter(ParameterContext p, ExtensionContext c) {
    User user = UsersApi.create(aUser().build());
    c.getStore(NS).put("user", user);   // стан у Store, а не в полі
    return user;
  }

  public void afterEach(ExtensionContext c) {
    User user = c.getStore(NS).remove("user", User.class);
    if (user != null) UsersApi.delete(user.id());   // прибирання навіть після падіння
  }
}

@Test
@ExtendWith(UserExtension.class)
void userSeesEmptyOrderHistory(User user) { /* користувач уже існує */ }
public class UserExtension implements ParameterResolver, AfterEachCallback {
  private static final Namespace NS = Namespace.create(UserExtension.class);

  public boolean supportsParameter(ParameterContext p, ExtensionContext c) {
    return p.getParameter().getType() == User.class;
  }

  public Object resolveParameter(ParameterContext p, ExtensionContext c) {
    User user = UsersApi.create(aUser().build());
    c.getStore(NS).put("user", user);   // state in the Store, not in a field
    return user;
  }

  public void afterEach(ExtensionContext c) {
    User user = c.getStore(NS).remove("user", User.class);
    if (user != null) UsersApi.delete(user.id());   // cleanup even after a failure
  }
}

@Test
@ExtendWith(UserExtension.class)
void userSeesEmptyOrderHistory(User user) { /* the user already exists */ }
Що в коді: JUnit питає кожен ParameterResolver, чи вміє він надати параметр типу User; afterEach розширення виконується незалежно від результату тесту, тому видалення даних гарантоване.
What the code shows: JUnit asks each ParameterResolver whether it can supply a parameter of type User; the extension's afterEach runs regardless of the test result, so data deletion is guaranteed.
💡 Простими словами: якщо базовий тестовий клас обріс десятком @BeforeEach «на всякий випадок» — розбий його на розширення, які тест підключає лише тоді, коли вони йому потрібні.
💡 In plain words: if your base test class has grown a dozen "just in case" @BeforeEach methods, split it into extensions that a test pulls in only when it needs them.
🗣 Як читати вголос Я перейшов від величезного BaseTest до кількох маленьких розширень. Одне через ParameterResolver дає тесту готового користувача і видаляє його після тесту, друге при падінні знімає скріншот і консоль браузера. Важливо тримати стан у ExtensionContext Store, а не в полях, бо тести в нас паралельні. І я враховую порядок колбеків: скріншот знімаю до того, як закривається драйвер.
🗣 How to say it out loud I moved away from one huge BaseTest to a few small extensions. One uses a ParameterResolver to hand the test a ready user and delete it afterwards; another grabs a screenshot and the browser console on failure. The important bit is keeping state in the ExtensionContext Store instead of fields, since our tests run in parallel. And I mind the callback order: the screenshot is taken before the driver gets closed.
▶ 6 8
61 Як підключити Allure до проєкту на JUnit5 і Maven і звідки береться папка allure-results? How do you wire Allure into a JUnit5 and Maven project, and where does the allure-results folder come from?
Allure і звітність у CI Allure and reporting in CI middle ▶

Що це — Allure працює у два етапи. Під час прогону адаптер allure-junit5 слухає події JUnit і пише JSON-файли результатів і вкладення в target/allure-results. Потім Allure CLI або allure-maven генерує з них HTML-звіт.

What it is — Allure works in two stages. During the run the allure-junit5 adapter listens to JUnit events and writes JSON result files and attachments into target/allure-results. Then the Allure CLI or allure-maven generates an HTML report from them.

Навіщо — розділення етапів дозволяє збирати результати з кількох джоб CI в одну папку і згенерувати єдиний звіт. Анотації @Step і @Attachment працюють через AspectJ, тому в argLine плагіна Surefire потрібно додати -javaagent з aspectjweaver. Шлях до результатів задається властивістю allure.results.directory.

Why — separating the stages lets you collect results from several CI jobs into one folder and generate a single report. The @Step and @Attachment annotations work through AspectJ, so the Surefire plugin's argLine needs a -javaagent with aspectjweaver. The results path is set by the allure.results.directory property.

Приклад — локально mvn clean test, потім allure serve target/allure-results піднімає тимчасовий сервер зі звітом; у CI — allure generate --clean -o allure-report і публікація папки як артефакту або на Pages.

Example — locally, mvn clean test followed by allure serve target/allure-results starts a temporary server with the report; on CI, allure generate --clean -o allure-report and publish the folder as an artifact or to Pages.

Підводні камені — найчастіша скарга «у звіті немає кроків» означає, що агент AspectJ не підключено: тест проходить, а @Step мовчки ігнорується. Друга — забутий clean: старі JSON лишаються, і звіт показує тести, яких уже немає. Третя — відкрити index.html подвійним кліком: браузер блокує завантаження даних з файлової системи, тому потрібен сервер.

Pitfalls — the most common complaint, "there are no steps in the report", means the AspectJ agent is not attached: the test passes and @Step is silently ignored. The second is a forgotten clean: old JSON files remain and the report shows tests that no longer exist. The third is opening index.html by double-click: the browser blocks loading data from the file system, so you need a server.

Уточнення на співбесіді — «Навіщо allure-bom?» BOM вирівнює версії всіх модулів Allure (junit5, rest-assured, selenide), щоб не ловити несумісність між ними.

Interview follow-up — "Why allure-bom?" The BOM aligns versions of all Allure modules (junit5, rest-assured, selenide) so you don't hit incompatibilities between them.

pom.xml — залежність і агент AspectJ для Surefire
pom.xml — dependency and the AspectJ agent for Surefire
<!-- версії Allure з BOM -->
<dependency>
  <groupId>io.qameta.allure</groupId>
  <artifactId>allure-junit5</artifactId>
  <scope>test</scope>
</dependency>

<plugin>
  <artifactId>maven-surefire-plugin</artifactId>
  <configuration>
    <!-- без агента @Step і @Attachment не спрацюють -->
    <argLine>
      -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar"
    </argLine>
    <systemPropertyVariables>
      <allure.results.directory>${project.build.directory}/allure-results</allure.results.directory>
    </systemPropertyVariables>
  </configuration>
</plugin>

<!-- далі: mvn clean test, потім allure serve target/allure-results -->
<!-- Allure versions come from the BOM -->
<dependency>
  <groupId>io.qameta.allure</groupId>
  <artifactId>allure-junit5</artifactId>
  <scope>test</scope>
</dependency>

<plugin>
  <artifactId>maven-surefire-plugin</artifactId>
  <configuration>
    <!-- without the agent @Step and @Attachment do nothing -->
    <argLine>
      -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar"
    </argLine>
    <systemPropertyVariables>
      <allure.results.directory>${project.build.directory}/allure-results</allure.results.directory>
    </systemPropertyVariables>
  </configuration>
</plugin>

<!-- then: mvn clean test, followed by allure serve target/allure-results -->
Що в коді: -javaagent вплітає аспекти Allure в байткод під час завантаження класів, а systemPropertyVariables передає тестовому JVM шлях, куди адаптер пише JSON.
What the code shows: -javaagent weaves Allure aspects into bytecode at class-load time, and systemPropertyVariables passes the test JVM the path where the adapter writes its JSON.
💡 Простими словами: allure-results — це сирі дані, allure-report — готовий сайт. У CI зберігай обидва: сирі потрібні для історії та злиття результатів кількох джоб.
💡 In plain words: allure-results is raw data, allure-report is the finished site. Keep both on CI: the raw data is what you need for history and for merging results from several jobs.
🗣 Як читати вголос Коли я підключаю Allure, то пояснюю команді, що є два кроки: тести пишуть JSON у allure-results, а звіт генерується окремо. У Maven це залежність allure-junit5 через BOM і агент aspectjweaver у argLine Surefire. Я вже двічі бачив, як люди скаржилися, що кроків у звіті немає, — щоразу причиною був відсутній агент. У CI ми генеруємо звіт окремою командою і викладаємо як артефакт.
🗣 How to say it out loud When I wire up Allure I explain to the team that there are two stages: tests write JSON into allure-results, and the report is generated separately. In Maven that means the allure-junit5 dependency via the BOM and the aspectjweaver agent in Surefire's argLine. Twice now I've seen people complain that the report has no steps, and both times the agent was missing. On CI we generate the report with a separate command and publish it as an artifact.
62 Як показати кроки тесту в Allure: анотація @Step чи Allure.step з лямбдою? How do you show test steps in Allure: the @Step annotation or Allure.step with a lambda?
Allure і звітність у CI Allure and reporting in CI junior ▶

Що це — Крок в Allure — це вкладений вузол у звіті тесту зі своєю назвою, статусом, тривалістю і власними вкладеннями. Створити його можна двома способами. Анотація @Step("Додати в кошик {sku}") над методом робить кожен виклик цього методу кроком, а імена параметрів у фігурних дужках підставляються реальними значеннями (підтримується й доступ до полів, наприклад {user.email}). Другий спосіб — Allure.step("назва", () -> { ... }): крок оголошується прямо в тілі тесту без окремого методу.

What it is — A step in Allure is a nested node in the test report with its own name, status, duration and attachments. There are two ways to create one. The @Step("Add {sku} to cart") annotation on a method turns every call of that method into a step, and parameter names in curly braces are replaced with the real values (field access such as {user.email} works too). The second way is Allure.step("name", () -> { ... }): the step is declared inline in the test body without a separate method.

Навіщо — Звіт із кроками читає не тільки автор тесту: розробник чи менеджер бачить, на якій бізнес-дії впав сценарій, не відкриваючи код. Анотацію зручно ставити на методи step-класів, page objects і API-клієнтів: позначив один раз — і всі тести отримали кроки. Лямбда підходить для короткого одноразового блоку, наприклад щоб загорнути групу перевірок.

Why — A report with steps is read by more people than the author: a developer or a manager sees which business action the scenario died on without opening the code. The annotation fits methods in step classes, page objects and API clients: mark it once and every test gets the steps. The lambda suits a short one-off block, for example wrapping a group of checks.

Приклад — метод addToCart("SKU-42", 2) з анотацією дає у звіті рядок «Додати в кошик товар SKU-42 у кількості 2», а якщо всередині він викликає інший анотований метод, у звіті з'являється дерево вкладених кроків.

Example — calling addToCart("SKU-42", 2) on an annotated method shows the line "Add SKU-42 to the cart, quantity 2" in the report, and if it calls another annotated method inside, the report shows a tree of nested steps.

Підводні камені — @Step працює через AspectJ: якщо javaagent не підключений у surefire, кроки мовчки зникають, а тест при цьому зелений. Крок на кожен клік робить звіт нечитабельним — кроки мають відповідати діям користувача, а не викликам WebDriver.

Pitfalls — @Step relies on AspectJ: without the javaagent in surefire the steps silently vanish while the test stays green. A step per click makes the report unreadable — steps should match user actions, not WebDriver calls.

Уточнення на співбесіді — «Чи класти assert у крок?» Так, перевірку варто загорнути в крок на кшталт «Перевірити суму замовлення»: тоді падіння підсвічується на конкретному кроці, а не десь у кінці тесту.

Interview follow-up — "Should an assert live inside a step?" Yes, wrapping it in a step such as "Verify order total" makes the failure highlight that exact step instead of somewhere at the end of the test.

Java — @Step на методі і Allure.step у тесті
Java — @Step on a method and Allure.step in a test
public class CartSteps {
    @Step("Додати в кошик товар {sku} у кількості {qty}")
    public void addToCart(String sku, int qty) {
        api.addItem(sku, qty);
    }
}

@Test
void totalIsRecalculated() {
    steps.addToCart("SKU-42", 2);
    Allure.step("Перевірити суму в кошику", () -> {
        assertThat(cart.total()).isEqualByComparingTo("59.98");
    });
}
public class CartSteps {
    @Step("Add {sku} to the cart, quantity {qty}")
    public void addToCart(String sku, int qty) {
        api.addItem(sku, qty);
    }
}

@Test
void totalIsRecalculated() {
    steps.addToCart("SKU-42", 2);
    Allure.step("Verify the cart total", () -> {
        assertThat(cart.total()).isEqualByComparingTo("59.98");
    });
}
Що в коді: {sku} і {qty} беруться з параметрів методу, тож назва кроку в звіті містить реальні дані. Лямбда в Allure.step робить групу перевірок окремим вузлом без створення нового методу.
What the code shows: {sku} and {qty} come from the method parameters, so the step name in the report carries real data. The lambda in Allure.step turns a group of checks into a separate node without creating a new method.
💡 Простими словами: Назва кроку має звучати як речення з тест-кейсу, яке зрозуміє людина без доступу до коду. Якщо крок називається «click» — це вже не крок, а шум.
💡 In plain words: A step name should read like a line from a test case that someone without access to the code can follow. A step called "click" is noise, not a step.
🗣 Як читати вголос Я ставлю @Step на методи в step-класах і API-клієнтах, бо тоді кроки з'являються у всіх тестах автоматично. Всередині самого тесту інколи беру Allure.step з лямбдою, коли хочу загорнути кілька перевірок в один читабельний блок. Одного разу ми тиждень дивувалися, чому звіт без кроків, а причина була в тому, що хтось прибрав aspectj-агента з конфігурації surefire. Тепер я завжди перевіряю це першим.
🗣 How to say it out loud I put @Step on methods in step classes and API clients, because then every test gets the steps for free. Inside a test I sometimes use Allure.step with a lambda when I want several checks to show up as one readable block. Once we spent a week wondering why the report had no steps at all, and it turned out someone had removed the aspectj agent from the surefire config. Now that is the first thing I check.
63 Як побудувати дерево Behaviors в Allure через @Epic, @Feature, @Story і навіщо потрібні @Severity та @Owner? How do you build the Allure Behaviors tree with @Epic, @Feature and @Story, and what are @Severity and @Owner for?
Allure і звітність у CI Allure and reporting in CI junior ▶

Що це — Анотації @Epic, @Feature і @Story додають до результату тесту мітки, з яких Allure будує вкладку Behaviors: епік містить фічі, фіча містить історії, а в історіях лежать тести. Їх можна ставити на клас (успадковують усі методи) і на окремий метод. @Severity задає важливість: BLOCKER, CRITICAL, NORMAL (за замовчуванням), MINOR, TRIVIAL. @Owner показує, до кого йти з питаннями, а @Description додає розгорнутий опис. Назву тесту у звіті Allure бере з JUnit-анотації @DisplayName.

What it is — The @Epic, @Feature and @Story annotations add labels to the test result, and Allure builds the Behaviors tab from them: an epic holds features, a feature holds stories, and stories hold tests. You can put them on the class (every method inherits them) or on a single method. @Severity sets importance: BLOCKER, CRITICAL, NORMAL (the default), MINOR, TRIVIAL. @Owner tells people whom to ask, and @Description adds a longer explanation. Allure takes the test name from the JUnit @DisplayName annotation.

Навіщо — Вкладка Suites групує тести за пакетами й класами, тобто так, як зручно автоматизатору. Behaviors групує їх за продуктом: продакт-оунер відкриває «Оформлення замовлення → Кошик» і бачить, що саме там червоне. Severity дає змогу відфільтрувати звіт і першими розбирати критичні падіння.

Why — The Suites tab groups tests by package and class, which is how an automation engineer thinks. Behaviors groups them by product: a product owner opens "Checkout → Cart" and sees what exactly is red there. Severity lets you filter the report and triage the critical failures first.

Приклад — клас CartTest позначений епіком і фічею, а кожен метод має свою історію, наприклад «Промокод», і severity відповідно до ризику.

Example — the CartTest class carries the epic and the feature, while each method has its own story, such as "Promo code", and a severity that reflects the risk.

Підводні камені — Якщо один автор пише «Checkout», а інший «Оформлення», дерево розпадається на дві гілки. Назви варто тримати в класі констант і використовувати з нього. Ще одна помилка — ставити CRITICAL на все підряд, після чого фільтр втрачає сенс.

Pitfalls — If one author writes "Checkout" and another writes "Order checkout", the tree splits into two branches. Keep the names in a constants class and reference them from there. Another mistake is marking everything CRITICAL, after which the filter is useless.

Уточнення на співбесіді — «Чи можна запустити тільки критичні тести за severity?» Ні, це лише метадані для звіту. Для вибіркового запуску потрібен JUnit @Tag("critical") і фільтр groups у surefire.

Interview follow-up — "Can I run only the critical tests by severity?" No, it is report metadata only. Selective runs need a JUnit @Tag("critical") and a groups filter in surefire.

Java — мітки для дерева Behaviors
Java — labels for the Behaviors tree
@Epic("Оформлення замовлення")
@Feature("Кошик")
@Owner("o.koval")
class CartTest {

    @Test
    @Story("Промокод")
    @Severity(SeverityLevel.CRITICAL)
    @DisplayName("Промокод SALE10 зменшує суму на 10%")
    @Description("Сума перераховується одразу після застосування коду")
    void promoCodeReducesTotal() {
        cart.applyPromo("SALE10");
        assertThat(cart.total()).isEqualByComparingTo("90.00");
    }
}
@Epic("Checkout")
@Feature("Cart")
@Owner("o.koval")
class CartTest {

    @Test
    @Story("Promo code")
    @Severity(SeverityLevel.CRITICAL)
    @DisplayName("Promo code SALE10 takes 10% off the total")
    @Description("The total is recalculated right after the code is applied")
    void promoCodeReducesTotal() {
        cart.applyPromo("SALE10");
        assertThat(cart.total()).isEqualByComparingTo("90.00");
    }
}
Що в коді: епік, фіча й власник задані на рівні класу й поширюються на всі тести в ньому. Історія та severity специфічні для методу, тому стоять над ним.
What the code shows: the epic, feature and owner are set at class level and apply to every test inside. The story and severity are specific to the method, so they sit on it.
💡 Простими словами: Suites — це карта коду, Behaviors — карта продукту. Якщо звіт відкривають люди поза QA, дерево Behaviors важливіше за назви пакетів.
💡 In plain words: Suites is a map of the code, Behaviors is a map of the product. If people outside QA open the report, the Behaviors tree matters more than package names.
🗣 Як читати вголос У нас епік і фіча завжди стоять на класі, а історія й severity — на методі. Назви я виніс у клас констант після того, як побачив у звіті три різні гілки для того самого кошика. Severity ми домовилися ставити за реальним ризиком, і критичних тестів у нас близько десяти відсотків. Завдяки цьому зранку я фільтрую звіт за критичними і за п'ять хвилин розумію, чи можна релізити.
🗣 How to say it out loud On my team the epic and feature always live on the class, while the story and severity go on the method. I moved the names into a constants class after seeing three different branches for the same cart in the report. We agreed to set severity by real risk, so only about ten percent of our tests are critical. That means in the morning I filter the report by critical and know within five minutes whether we can release.
64 Як зв'язати автотест із задачею в Jira і тест-кейсом у TMS через @Issue, @TmsLink та @Link? How do you link an automated test to a Jira ticket and a TMS test case with @Issue, @TmsLink and @Link?
Allure і звітність у CI Allure and reporting in CI middle ▶

Що це — @Issue("SHOP-3141") і @TmsLink("QA-1287") зберігають у результаті тесту лише ідентифікатор. Повну адресу Allure будує з шаблону у файлі allure.properties в src/test/resources: allure.link.issue.pattern і allure.link.tms.pattern, де {} замінюється на ID. @Link(name = "...", url = "...") потрібен для довільних посилань, наприклад на специфікацію API чи сторінку в Confluence.

What it is — @Issue("SHOP-3141") and @TmsLink("QA-1287") store only an identifier in the test result. Allure builds the full URL from a pattern in allure.properties under src/test/resources: allure.link.issue.pattern and allure.link.tms.pattern, where {} is replaced by the ID. @Link(name = "...", url = "...") covers arbitrary links such as an API spec or a Confluence page.

Навіщо — Це відповідає на два часті запитання. Перше — який автотест покриває тест-кейс QA-1287, тобто базова трасованість. Друге — чому цей тест червоний: якщо над ним висить @Issue з відкритим багом, розбирати падіння вдруге не треба. Шаблон в одному файлі означає, що після переїзду Jira на новий домен міняється один рядок, а не тисяча анотацій.

Why — It answers two frequent questions. First, which automated test covers test case QA-1287, which is basic traceability. Second, why is this test red: if it carries an @Issue pointing to an open bug, nobody needs to triage it again. Keeping the pattern in one file means that when Jira moves to a new domain you change one line, not a thousand annotations.

Приклад — тест часткового повернення коштів падає через відомий баг SHOP-3141. Ми додаємо @Issue, і в Allure поруч із падінням з'являється клікабельне посилання на тікет, а в описі видно, що проблема вже відома.

Example — a partial refund test fails because of the known bug SHOP-3141. We add @Issue, and Allure shows a clickable ticket link next to the failure, so everyone can see the problem is already known.

Підводні камені — Посилання гниють: баг закрили, а анотація лишилася, і тест, який знову впаде, помилково вважають відомою проблемою. Повні URL у кожній анотації замість шаблону теж погана ідея. Ще одна пастка — ключ властивості має збігатися з типом посилання, інакше Allure покаже голий ID без адреси.

Pitfalls — Links rot: the bug gets closed, the annotation stays, and a test that breaks again is mistaken for a known issue. Hardcoding full URLs in every annotation instead of a pattern is also a bad idea. Another trap is that the property key must match the link type, or Allure shows a bare ID with no URL.

Уточнення на співбесіді — «Як тримати посилання актуальними?» Простий скрипт у нічному CI збирає всі @Issue з результатів, питає статуси в Jira API і повідомляє про тести, чиї баги вже закриті.

Interview follow-up — "How do you keep the links current?" A small script in the nightly CI collects every @Issue from the results, queries the Jira API for statuses and reports tests whose bugs are already closed.

allure.properties і анотації посилань
allure.properties and link annotations
# src/test/resources/allure.properties
allure.results.directory=target/allure-results
allure.link.issue.pattern=https://jira.example.com/browse/{}
allure.link.tms.pattern=https://tms.example.com/case/{}

// RefundTest.java
@Test
@TmsLink("QA-1287")
@Issue("SHOP-3141")
@Link(name = "Refund API spec", url = "https://docs.example.com/orders#refund")
void partialRefundKeepsOrderOpen() {
    Refund refund = orders.refund(orderId, new BigDecimal("10.00"));
    assertThat(orders.get(orderId).status()).isEqualTo(OrderStatus.OPEN);
}
Що в коді: у тесті лише ID, а домени Jira і TMS живуть в одному файлі властивостей. @Link несе повну адресу, бо для нього немає шаблону.
What the code shows: the test holds only IDs, while the Jira and TMS domains live in one properties file. @Link carries a full URL because there is no pattern for it.
💡 Простими словами: @TmsLink відповідає на «що покриває цей тест», @Issue — на «чому він червоний». Обидва корисні, лише доки їх хтось регулярно звіряє з реальністю.
💡 In plain words: @TmsLink answers "what does this test cover", @Issue answers "why is it red". Both are only useful while someone regularly checks them against reality.
🗣 Як читати вголос Кожен наш автотест має @TmsLink на кейс у TestRail, і з результатів Allure ми будуємо звіт покриття для релізу. @Issue я ставлю лише тоді, коли тест падає через уже заведений баг, і обов'язково з коментарем. Одного разу закритий баг повернувся регресією, а тест через стару анотацію всі вважали відомою проблемою. Після цього ми додали нічну перевірку статусів тікетів.
🗣 How to say it out loud Every automated test on our project has a @TmsLink to its TestRail case, and we build the release coverage report from the Allure results. I add @Issue only when a test fails because of a bug that is already filed, and always with a comment. Once a closed bug came back as a regression, and everyone ignored the failing test because of a stale annotation. After that we added a nightly check of ticket statuses.
65 Як автоматично додавати в Allure скріншот, page source і HTTP-обмін, коли тест падає? How do you automatically attach a screenshot, page source and HTTP exchange to Allure when a test fails?
Allure і звітність у CI Allure and reporting in CI middle ▶

Що це — Вкладення в Allure додаються через Allure.addAttachment(name, type, content, extension) або метод з анотацією @Attachment(value = "...", type = "image/png"), який повертає byte[]. Щоб це відбувалося без ручних викликів у кожному тесті, логіку кладуть у JUnit5-розширення, яке перевіряє, чи був виняток. Для API є готовий фільтр AllureRestAssured: він прикріплює кожен запит і відповідь.

What it is — Allure attachments are added with Allure.addAttachment(name, type, content, extension) or with a method annotated @Attachment(value = "...", type = "image/png") that returns byte[]. To make it happen without manual calls in every test, the logic goes into a JUnit5 extension that checks whether an exception occurred. For APIs there is a ready filter, AllureRestAssured, which attaches every request and response.

Навіщо — Падіння в нічному прогоні часто не відтворюється локально. Скріншот показує стан екрана, page source дає DOM у момент помилки, а HTTP-обмін пояснює, що бекенд реально повернув. З цими трьома речами більшість падінь можна розібрати без перезапуску.

Why — A failure in the nightly run often does not reproduce locally. The screenshot shows the screen state, the page source gives the DOM at the moment of the error, and the HTTP exchange explains what the backend really returned. With those three you can triage most failures without a rerun.

Приклад — розширення реалізує AfterTestExecutionCallback і дивиться на getExecutionException(). Цей колбек виконується до методів @AfterEach, тож браузер ще живий.

Example — the extension implements AfterTestExecutionCallback and inspects getExecutionException(). This callback runs before the @AfterEach methods, so the browser is still alive.

Підводні камені — Популярна помилка — робити скріншот у TestWatcher.testFailed: він викликається вже після @AfterEach, де драйвер закрито, і замість скріншота маємо ще один виняток. Друга — знімати скріншот після кожного кроку: allure-results розростаються до гігабайтів, а CI-артефакти впираються в ліміти. Логи HTTP з токенами варто маскувати.

Pitfalls — A common mistake is taking the screenshot in TestWatcher.testFailed: it runs after @AfterEach, where the driver is already quit, so instead of a screenshot you get another exception. The second is a screenshot after every step: allure-results grows to gigabytes and CI artifacts hit their limits. HTTP logs containing tokens should be masked.

Уточнення на співбесіді — «А відео?» Відео пишемо для всіх, але зберігаємо лише для впалих тестів: у Selenoid чи Playwright це налаштування, а не код.

Interview follow-up — "What about video?" We record it for everything but keep it only for failed tests: in Selenoid or Playwright that is a setting, not code.

Java — розширення JUnit5 для доказів при падінні
Java — JUnit5 extension for evidence on failure
public class EvidenceOnFailure implements AfterTestExecutionCallback {
    @Override
    public void afterTestExecution(ExtensionContext ctx) {
        if (ctx.getExecutionException().isEmpty()) return; // тест зелений
        WebDriver driver = DriverHolder.get();        // драйвер ще не закритий
        byte[] png = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
        Allure.addAttachment("Скріншот у момент падіння", "image/png",
                new ByteArrayInputStream(png), "png");
        Allure.addAttachment("Page source", "text/html",
                driver.getPageSource(), "html");
    }
}

// API-тести: кожен запит і відповідь стають вкладенням
given().filter(new AllureRestAssured())
    .when().get("/orders/42")
    .then().statusCode(200);
public class EvidenceOnFailure implements AfterTestExecutionCallback {
    @Override
    public void afterTestExecution(ExtensionContext ctx) {
        if (ctx.getExecutionException().isEmpty()) return; // test passed
        WebDriver driver = DriverHolder.get();        // driver not quit yet
        byte[] png = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
        Allure.addAttachment("Screenshot at failure", "image/png",
                new ByteArrayInputStream(png), "png");
        Allure.addAttachment("Page source", "text/html",
                driver.getPageSource(), "html");
    }
}

// API tests: every request and response becomes an attachment
given().filter(new AllureRestAssured())
    .when().get("/orders/42")
    .then().statusCode(200);
Що в коді: getExecutionException() повертає Optional з винятком тесту, тож вкладення створюються тільки для червоних тестів. Розширення підключається через @ExtendWith(EvidenceOnFailure.class) на базовому класі.
What the code shows: getExecutionException() returns an Optional with the test's exception, so attachments are created only for red tests. The extension is registered with @ExtendWith(EvidenceOnFailure.class) on a base class.
💡 Простими словами: Збирай докази, поки місце злочину ще не прибрали: після @AfterEach браузера вже немає. Тому AfterTestExecutionCallback, а не TestWatcher.
💡 In plain words: Collect the evidence before the crime scene is cleaned up: after @AfterEach the browser is gone. That is why AfterTestExecutionCallback, not TestWatcher.
🗣 Як читати вголос У моєму фреймворку є одне розширення, яке при падінні кладе в Allure скріншот, DOM і логи консолі браузера, а для API ми просто додаємо фільтр AllureRestAssured у базову специфікацію. Спочатку я робив це в TestWatcher і отримував порожні вкладення, бо драйвер на той момент уже був закритий. Перейшов на AfterTestExecutionCallback, і все запрацювало. Скріншоти після кожного кроку ми свідомо не робимо, щоб артефакти не роздувалися.
🗣 How to say it out loud My framework has one extension that, on failure, puts a screenshot, the DOM and the browser console logs into Allure, and for APIs we simply add the AllureRestAssured filter to the base spec. At first I did it in a TestWatcher and got empty attachments, because by then the driver was already closed. I switched to AfterTestExecutionCallback and it worked. We deliberately skip screenshots after every step so the artifacts stay small.
66 Як підключити Allure до Playwright на TypeScript і коли достатньо вбудованого HTML-звіту? How do you plug Allure into Playwright with TypeScript, and when is the built-in HTML report enough?
Allure і звітність у CI Allure and reporting in CI middle ▶

Що це — Пакет allure-playwright — це репортер Playwright Test: його додають у масив reporter у playwright.config.ts, і під час прогону він пише JSON-файли в allure-results. Кроки з test.step() автоматично стають кроками Allure, а вкладення з testInfo.attach(), скріншоти й trace, налаштовані в секції use, прикріплюються до тесту. Мітки на кшталт epic, feature і severity задаються функціями з пакета allure-js-commons.

What it is — The allure-playwright package is a Playwright Test reporter: you add it to the reporter array in playwright.config.ts, and during the run it writes JSON files to allure-results. Steps from test.step() automatically become Allure steps, and attachments from testInfo.attach() plus the screenshots and traces configured under use are attached to the test. Labels such as epic, feature and severity are set with functions from the allure-js-commons package.

Навіщо і коли — Вбудований HTML-звіт Playwright чудовий для одного прогону: він відкриває trace viewer, показує ретраї й діфи скріншотів. Allure потрібен, коли важливі історія і тренди між збірками, категорії падінь, групування за продуктом або єдиний звіт для кількох фреймворків, наприклад API-тестів на Java і UI-тестів на TypeScript.

Why and when — Playwright's built-in HTML report is great for a single run: it opens the trace viewer and shows retries and screenshot diffs. Allure is worth it when you need history and trends across builds, failure categories, grouping by product, or one report for several frameworks, for example Java API tests and TypeScript UI tests.

Приклад — у конфігурації два репортери: list для консолі CI і allure-playwright для звіту. Тест задає фічу й severity, загортає дії в test.step і прикріплює JSON кошика.

Example — the config has two reporters: list for the CI console and allure-playwright for the report. The test sets a feature and severity, wraps actions in test.step and attaches the cart JSON.

Підводні камені — Репортер лише пише результати, а сам звіт генерує окремий CLI Allure, який треба встановити в CI. Версії allure-playwright і allure-js-commons мають збігатися, інакше мітки мовчки губляться. Якщо лишити тільки Allure, команда втрачає зручний перегляд trace — тому зазвичай тримають обидва звіти.

Pitfalls — The reporter only writes results; the report itself is generated by a separate Allure CLI that has to be installed in CI. The versions of allure-playwright and allure-js-commons must match, or labels silently disappear. If you keep only Allure, the team loses the convenient trace viewing, so teams usually keep both reports.

Уточнення на співбесіді — «Чи потрапить trace в Allure?» Так, як вкладення-архів, але зручно його відкривати все одно через npx playwright show-trace або trace.playwright.dev.

Interview follow-up — "Does the trace end up in Allure?" Yes, as a zip attachment, but it is still easier to open via npx playwright show-trace or trace.playwright.dev.

TypeScript — конфіг репортерів і мітки в тесті
TypeScript — reporter config and labels in a test
// playwright.config.ts
export default defineConfig({
  reporter: [['list'], ['allure-playwright', { resultsDir: 'allure-results' }]],
  use: { screenshot: 'only-on-failure', trace: 'retain-on-failure' },
});

// checkout.spec.ts
import * as allure from 'allure-js-commons';

test('гість оформлює замовлення', async ({ page }, testInfo) => {
  await allure.feature('Оформлення замовлення');
  await allure.severity('critical');
  const cart = { sku: 'SKU-42', qty: 1 };
  await test.step('Додати товар у кошик', async () => {
    await page.getByTestId('add-to-cart').click();
  });
  await testInfo.attach('кошик.json', {
    body: JSON.stringify(cart), contentType: 'application/json',
  });
});
// playwright.config.ts
export default defineConfig({
  reporter: [['list'], ['allure-playwright', { resultsDir: 'allure-results' }]],
  use: { screenshot: 'only-on-failure', trace: 'retain-on-failure' },
});

// checkout.spec.ts
import * as allure from 'allure-js-commons';

test('guest places an order', async ({ page }, testInfo) => {
  await allure.feature('Checkout');
  await allure.severity('critical');
  const cart = { sku: 'SKU-42', qty: 1 };
  await test.step('Add the item to the cart', async () => {
    await page.getByTestId('add-to-cart').click();
  });
  await testInfo.attach('cart.json', {
    body: JSON.stringify(cart), contentType: 'application/json',
  });
});
Що в коді: репортери задаються масивом пар «назва, опції», тож Allure додається поряд зі звичайним виводом. test.step і testInfo.attach — стандартне API Playwright, яке Allure просто підхоплює.
What the code shows: reporters are an array of "name, options" pairs, so Allure sits next to the normal output. test.step and testInfo.attach are plain Playwright API that Allure simply picks up.
💡 Простими словами: HTML-звіт Playwright відповідає на «що сталося в цьому прогоні», Allure — на «що відбувається з тестами протягом місяця». Пиши кроки через test.step, і вони будуть видні в обох.
💡 In plain words: Playwright's HTML report answers "what happened in this run", Allure answers "what has been happening to the tests over the month". Write steps with test.step and they show up in both.
🗣 Як читати вголос На TypeScript-проєкті я тримаю два репортери: HTML від Playwright для розробників, бо там найзручніший trace viewer, і allure-playwright, бо наші API-тести на Java теж пишуть в Allure і менеджмент дивиться один звіт. Кроки я пишу тільки через test.step, без Allure-специфічних обгорток, тож тести не залежать від репортера. Якщо Allure колись приберуть, змінимо один рядок конфігурації.
🗣 How to say it out loud On a TypeScript project I keep two reporters: Playwright's HTML one for developers, because its trace viewer is the most convenient, and allure-playwright, because our Java API tests also write to Allure and management looks at a single report. I write steps only with test.step, without Allure-specific wrappers, so tests do not depend on the reporter. If we ever drop Allure, we change one line of config.
67 Як зберегти історію і тренди Allure між запусками в CI та класифікувати падіння через categories.json? How do you keep Allure history and trends between CI runs and classify failures with categories.json?
Allure і звітність у CI Allure and reporting in CI senior ▶

Що це — Звіт Allure 2 генерується з папки allure-results і сам по собі нічого не пам'ятає. Графіки трендів і позначка flaky з'являються, лише якщо перед генерацією скопіювати папку history з попереднього звіту в allure-results/history. У CI це означає: завантажити минулий звіт (з гілки gh-pages, S3 чи артефакту), перенести історію, згенерувати новий звіт і опублікувати його туди ж.

What it is — An Allure 2 report is generated from the allure-results folder and remembers nothing on its own. Trend charts and the flaky marker appear only if, before generating, you copy the history folder from the previous report into allure-results/history. In CI that means: fetch the last report (from a gh-pages branch, S3 or an artifact), carry the history over, generate the new report and publish it to the same place.

Класифікація — За замовчуванням Allure ділить падіння на Product defects (статус failed, тобто AssertionError) і Test defects (broken, будь-який інший виняток). Файл categories.json у allure-results додає власні категорії за messageRegex і traceRegex: «Інфраструктура» для Connection refused і 502, «Таймаути UI» для TimeoutException. Поряд кладуть environment.properties зі стендом і версією збірки та executor.json

Classification — By default Allure splits failures into Product defects (status failed, meaning AssertionError) and Test defects (broken, any other exception). A categories.json file in allure-results adds custom categories by messageRegex and traceRegex: "Infrastructure" for Connection refused and 502, "UI timeouts" for TimeoutException. Next to it go environment.properties with the stand and build version, and executor.json with a link to the CI build.

Компроміси — Історію варто вести лише для main або нічного прогону: якщо змішати фіча-гілки, тренд показує шум чужих незавершених змін. Звіти на Pages ростуть, тому потрібна ротація, наприклад останні 20 збірок. Категорії — це регулярні вирази, які треба переглядати, інакше нова інфраструктурна помилка тихо потрапляє в Product defects.

Trade-offs — Keep history only for main or the nightly run: if you mix in feature branches, the trend shows the noise of other people's unfinished work. Reports on Pages keep growing, so you need rotation, for example the last 20 builds. Categories are regular expressions that need reviewing, or a new kind of infrastructure error silently lands in Product defects.

Уточнення на співбесіді — «Навіщо категорії, якщо є статуси?» Вони дають метрику: якщо 60% падінь за тиждень — інфраструктура, інвестувати треба в стенд, а не в переписування тестів.

Interview follow-up — "Why categories if you have statuses?" They give you a metric: if 60% of the week's failures are infrastructure, you invest in the environment, not in rewriting tests.

GitHub Actions — перенесення історії; categories.json
GitHub Actions — carrying history over; categories.json
- name: Отримати попередній звіт
  uses: actions/checkout@v4
  with: { ref: gh-pages, path: gh-pages }
  continue-on-error: true   # першого разу гілки ще немає
- name: Перенести історію і категорії
  run: |
    mkdir -p allure-results/history
    cp -r gh-pages/history/. allure-results/history/ || true
    cp ci/categories.json ci/environment.properties allure-results/
- name: Згенерувати звіт
  run: allure generate allure-results -o allure-report --clean

// ci/categories.json
[
  { "name": "Інфраструктура", "matchedStatuses": ["broken"],
    "messageRegex": ".*(Connection refused|502 Bad Gateway).*" },
  { "name": "Таймаути UI", "matchedStatuses": ["broken", "failed"],
    "traceRegex": ".*TimeoutException.*" }
]
- name: Fetch the previous report
  uses: actions/checkout@v4
  with: { ref: gh-pages, path: gh-pages }
  continue-on-error: true   # the branch does not exist on the first run
- name: Carry over history and categories
  run: |
    mkdir -p allure-results/history
    cp -r gh-pages/history/. allure-results/history/ || true
    cp ci/categories.json ci/environment.properties allure-results/
- name: Generate the report
  run: allure generate allure-results -o allure-report --clean

// ci/categories.json
[
  { "name": "Infrastructure", "matchedStatuses": ["broken"],
    "messageRegex": ".*(Connection refused|502 Bad Gateway).*" },
  { "name": "UI timeouts", "matchedStatuses": ["broken", "failed"],
    "traceRegex": ".*TimeoutException.*" }
]
Що в коді: історія береться з уже опублікованого звіту й кладеться в нові результати до генерації. Перша категорія, чий регулярний вираз збігся, забирає падіння собі, тому порядок записів у JSON має значення.
What the code shows: the history is taken from the already published report and placed into the new results before generation. The first category whose regex matches claims the failure, so the order of entries in the JSON matters.
💡 Простими словами: Allure не має бази даних — його пам'ять це папка history, яку ти сам переносиш зі збірки в збірку. Забув скопіювати — тренд обнулився.
💡 In plain words: Allure has no database — its memory is the history folder you carry from build to build yourself. Forget to copy it and the trend resets.
🗣 Як читати вголос На останньому проєкті звіт публікувався на GitHub Pages тільки з нічного прогону на main, і перед генерацією ми підтягували history з попередньої версії. Категорії я налаштував, коли помітив, що половина червоного — це 502 від стенду, а виглядало як наші баги. Після цього ми щотижня показували на ретро частку інфраструктурних падінь, і DevOps отримали аргумент виділити окремий стенд для автотестів.
🗣 How to say it out loud On my last project the report was published to GitHub Pages only from the nightly run on main, and before generating we pulled history from the previous version. I set up categories after noticing that half of the red was 502s from the environment, even though it looked like our bugs. From then on we showed the share of infrastructure failures at every retro, and that gave DevOps the argument to set up a dedicated stand for automated tests.
68 Навіщо публікувати JUnit XML у CI, якщо в проєкті вже є Allure? Why publish JUnit XML in CI when the project already has Allure?
Allure і звітність у CI Allure and reporting in CI middle ▶

Що це — JUnit XML — це фактичний стандарт машинозчитуваних результатів: елементи testsuite, testcase, failure, error, skipped. Його без додаткових налаштувань пишуть Maven Surefire (target/surefire-reports/TEST-*.xml), Failsafe (target/failsafe-reports), Gradle (build/test-results/test), а в Playwright є вбудований репортер junit.

What it is — JUnit XML is the de facto standard for machine-readable results: the testsuite, testcase, failure, error and skipped elements. Maven Surefire writes it out of the box (target/surefire-reports/TEST-*.xml), so do Failsafe (target/failsafe-reports) and Gradle (build/test-results/test), and Playwright has a built-in junit reporter.

Навіщо — CI-системи читають цей формат нативно. GitLab через artifacts:reports:junit показує впалі тести прямо у віджеті merge request. Jenkins-крок junit будує тренд і позначає збірку як UNSTABLE. У GitHub Actions екшени на кшталт mikepenz/action-junit-report перетворюють XML на анотації в PR і зведення в check. Розробник бачить, що зламалося, там, де він і так працює, не відкриваючи окремий звіт. На цих же файлах будується аналітика flaky-тестів у багатьох CI.

Why — CI systems read this format natively. GitLab uses artifacts:reports:junit to show failed tests right in the merge request widget. The Jenkins junit step builds a trend and marks the build UNSTABLE. In GitHub Actions, actions such as mikepenz/action-junit-report turn the XML into PR annotations and a check summary. A developer sees what broke where they already work, without opening a separate report. Many CI flaky-test analytics are built on the same files.

Приклад — Allure лишається для глибокого розбору з кроками й вкладеннями, а XML дає швидку відповідь у PR: «впав OrderApiTest.refund, ось повідомлення».

Example — Allure stays for deep analysis with steps and attachments, while the XML gives a quick answer in the PR: "OrderApiTest.refund failed, here is the message".

Підводні камені — Без if: always() крок публікації пропускається саме тоді, коли тести впали. При шардингу однакові імена файлів з різних джобів перезаписують один одного. Шлях outputFile для Playwright треба вказати явно, інакше звіт іде в stdout.

Pitfalls — Without if: always() the publish step is skipped exactly when the tests failed. With sharding, identical file names from different jobs overwrite each other. The Playwright outputFile path must be set explicitly, otherwise the report goes to stdout.

Уточнення на співбесіді — «Чим failure відрізняється від error у XML?» Failure — це невдала перевірка, error — несподіваний виняток; це та сама різниця, що між failed і broken в Allure.

Interview follow-up — "How does failure differ from error in the XML?" Failure is a failed assertion, error is an unexpected exception; it is the same split as failed versus broken in Allure.

CI YAML — публікація JUnit XML у GitHub Actions і GitLab
CI YAML — publishing JUnit XML in GitHub Actions and GitLab
# GitHub Actions, Java
- name: Тести
  run: mvn -B verify
- name: Результати в PR
  if: always()          # публікувати і при червоних тестах
  uses: mikepenz/action-junit-report@v4
  with:
    report_paths: '**/target/surefire-reports/TEST-*.xml'

# GitLab CI, Playwright (у конфігу: reporter junit з outputFile results/junit.xml)
ui-tests:
  script: npx playwright test
  artifacts:
    when: always
    reports:
      junit: results/junit.xml
# GitHub Actions, Java
- name: Tests
  run: mvn -B verify
- name: Results in the PR
  if: always()          # publish even when tests are red
  uses: mikepenz/action-junit-report@v4
  with:
    report_paths: '**/target/surefire-reports/TEST-*.xml'

# GitLab CI, Playwright (config: junit reporter with outputFile results/junit.xml)
ui-tests:
  script: npx playwright test
  artifacts:
    when: always
    reports:
      junit: results/junit.xml
Що в коді: if: always() і when: always роблять одне й те саме в різних CI — запускають публікацію незалежно від статусу попередніх кроків. Glob-шаблон збирає XML з усіх модулів Maven.
What the code shows: if: always() and when: always do the same thing in different CIs — run the publishing regardless of the previous steps' status. The glob pattern collects XML from every Maven module.
💡 Простими словами: JUnit XML — для машин і для PR, Allure — для людини, яка розбирає падіння. Перше коштує нуль налаштувань, тому немає причин від нього відмовлятися.
💡 In plain words: JUnit XML is for machines and the PR, Allure is for the person triaging the failure. The first costs zero setup, so there is no reason to give it up.
🗣 Як читати вголос Я завжди публікую JUnit XML, навіть коли є гарний Allure, бо розробники не відкривають окремі звіти — вони дивляться на PR. Коли ми додали анотації з впалими тестами прямо в pull request, кількість питань «а чому червоне» в чаті помітно впала. Allure при цьому ніхто не прибирав: туди йдуть, коли треба глянути скріншот чи HTTP-обмін. Один раз я забув if: always(), і звіт з'являвся лише для зелених збірок, тобто тоді, коли він нікому не потрібен.
🗣 How to say it out loud I always publish JUnit XML, even with a nice Allure setup, because developers do not open separate reports — they look at the PR. When we added annotations with the failed tests right in the pull request, the "why is it red" questions in chat dropped noticeably. Nobody removed Allure; people go there when they need a screenshot or an HTTP exchange. Once I forgot if: always(), and the report only showed up for green builds, exactly when nobody needs it.
▶ 7 6
69 Яку політику локаторів ви закріплюєте в команді і чому data-testid стоїть першим? What locator policy do you set for the team, and why does data-testid come first?
Локатори й очікування в коді фреймворку Locators and waits in framework code middle ▶

Що це — Політика локаторів — це письмовий порядок пріоритетів, якого дотримуються всі автори тестів. Типовий варіант: спершу доступна роль і підпис (getByRole, getByLabel у Playwright) або окремий тестовий атрибут data-testid; далі стабільний id; потім короткий CSS за атрибутами; XPath — лише коли треба знайти елемент за текстом чи відносно сусіда і нічого іншого немає.

What it is — A locator policy is a written priority order every test author follows. A typical version: first the accessible role and label (getByRole, getByLabel in Playwright) or a dedicated test attribute data-testid; then a stable id; then short attribute-based CSS; XPath only when you must find an element by text or relative to a neighbour and nothing else works.

Навіщо — Більшість поломок UI-тестів після редизайну — це локатори. CSS-класи часто генеруються (CSS Modules чи styled-components дають щось на кшталт .btn_x7f2a), структура DOM змінюється від обгортки, а локатор за текстом ламається від перекладу. Тестовий атрибут — це контракт: розробник знає, що його не можна мовчки видалити.

Why — Most UI test breakages after a redesign are locators. CSS classes are often generated (CSS Modules or styled-components produce something like .btn_x7f2a), the DOM structure changes when someone adds a wrapper, and a text locator breaks on translation. A test attribute is a contract: the developer knows it cannot be silently removed.

Приклад — домовляємося з фронтендом про формат імен за доменом (checkout-submit, cart-row), додаємо атрибути в Definition of Done задачі, а для списків ставимо id на рядок і фільтруємо за текстом усередині. Крихкий //div[3]/div/button[2] замінюється на пошук за checkout-submit.

Example — we agree a domain-based naming format with the frontend (checkout-submit, cart-row), add the attributes to the task's Definition of Done, and for lists we put the id on the row and filter by the text inside. The fragile //div[3]/div/button[2] becomes a lookup by checkout-submit.

Підводні камені — Тестові атрибути на кожному div перетворюються на новий шум; вони потрібні на інтерактивних і ключових елементах. Якщо брати тільки test id, тести перестають помічати проблеми доступності, тому для кнопок і полів локатор за роллю часто кращий. Дублікати id на сторінці дають неоднозначні збіги.

Pitfalls — Test attributes on every div become new noise; they belong on interactive and key elements. Relying only on test ids makes tests blind to accessibility problems, so for buttons and inputs a role locator is often better. Duplicate ids on a page give ambiguous matches.

Уточнення на співбесіді — «У Selenium немає getByTestId, що робите?» Пишемо хелпер byTestId(), що будує CSS за атрибутом, і використовуємо тільки його.

Interview follow-up — "Selenium has no getByTestId, what do you do?" We write a byTestId() helper that builds attribute CSS and use only that.

Java і TypeScript — крихкі та стабільні локатори
Java and TypeScript — fragile and stable locators
// Погано: залежить від верстки і згенерованих класів
By.xpath("//div[3]/div/button[2]");
By.cssSelector(".css-1x2y9 > span");

// Добре: контракт із розробниками
public static By byTestId(String id) {
    return By.cssSelector("[data-testid='" + id + "']");
}
driver.findElement(byTestId("checkout-submit")).click();

// Playwright: роль для кнопок, test id для рядків списку
await page.getByRole('button', { name: 'Оформити замовлення' }).click();
await page.getByTestId('cart-row').filter({ hasText: 'SKU-42' })
  .getByTestId('remove').click();
// Bad: depends on layout and generated classes
By.xpath("//div[3]/div/button[2]");
By.cssSelector(".css-1x2y9 > span");

// Good: a contract with developers
public static By byTestId(String id) {
    return By.cssSelector("[data-testid='" + id + "']");
}
driver.findElement(byTestId("checkout-submit")).click();

// Playwright: role for buttons, test id for list rows
await page.getByRole('button', { name: 'Place order' }).click();
await page.getByTestId('cart-row').filter({ hasText: 'SKU-42' })
  .getByTestId('remove').click();
Що в коді: хелпер byTestId ховає формат селектора в одному місці, тож перейменування атрибута — це одна правка. У Playwright атрибут для getByTestId можна змінити через testIdAttribute у конфігурації.
What the code shows: the byTestId helper keeps the selector format in one place, so renaming the attribute is a single edit. In Playwright the attribute used by getByTestId can be changed via testIdAttribute in the config.
💡 Простими словами: Хороший локатор описує, що це за елемент для користувача, а не де він лежить у DOM. Якщо локатор має індекс у квадратних дужках — він зламається першим.
💡 In plain words: A good locator describes what the element is to the user, not where it sits in the DOM. If a locator has an index in square brackets, it will be the first to break.
🗣 Як читати вголос Політику локаторів я оформлюю як коротку сторінку у вікі і як пункт у чек-листі рев'ю. Першими йдуть роль і підпис, бо так ми заодно ловимо проблеми доступності, а data-testid — для списків і складних віджетів. З фронтендерами домовилися, що атрибути додаються разом із фічею, а не коли тест уже впав. Після цього кількість падінь через редизайн у нас зменшилася в рази, бо локатори просто перестали залежати від верстки.
🗣 How to say it out loud I write the locator policy as a short wiki page and as an item in the review checklist. Role and label come first, because that way we also catch accessibility issues, and data-testid is for lists and complex widgets. We agreed with the frontend developers that attributes are added together with the feature, not after a test has already failed. After that, breakages caused by redesigns dropped several times over, because locators simply stopped depending on the layout.
70 Чим findElement відрізняється від findElements і як перевірити, що елемента немає, без try/catch? How does findElement differ from findElements, and how do you check that an element is absent without try/catch?
Локатори й очікування в коді фреймворку Locators and waits in framework code junior ▶

Що це — findElement(By) повертає перший знайдений WebElement або кидає NoSuchElementException, якщо нічого не знайшлося. findElements(By) повертає List<WebElement> і ніколи не кидає виняток через відсутність: якщо збігів немає, список просто порожній.

What it is — findElement(By) returns the first matching WebElement or throws NoSuchElementException when nothing matches. findElements(By) returns a List<WebElement> and never throws because of absence: with no matches the list is simply empty.

Навіщо — Різниця важлива для перевірок «елемента не повинно бути»: промо-банера для преміум-користувача, кнопки видалення для гостя. Ловити виняток у тесті — поганий стиль, а findElements(...).isEmpty() читається як звичайна умова.

Why — The difference matters for "this must not be here" checks: a promo banner for a premium user, a delete button for a guest. Catching an exception inside a test is poor style, while findElements(...).isEmpty() reads like an ordinary condition.

Приклад — Для перевірки прав гостя я роблю assertThat(driver.findElements(deleteBtn)).isEmpty(). Якщо ж елемент має зникнути після дії (спінер), краще явне очікування invisibilityOfElementLocated, бо одноразова перевірка може спрацювати до того, як спінер з'явиться.

Example — To verify guest permissions I write assertThat(driver.findElements(deleteBtn)).isEmpty(). If the element has to disappear after an action (a spinner), an explicit invisibilityOfElementLocated wait is better, because a one-off check may run before the spinner even shows up.

Підводні камені — Якщо ввімкнено implicit wait, findElements для відсутнього елемента чекає весь таймаут, перш ніж повернути порожній список. Десять таких перевірок з implicit wait 10 с — це зайві 100 секунд прогону.

Pitfalls — With an implicit wait enabled, findElements on an absent element waits for the full timeout before returning the empty list. Ten such checks with a 10 s implicit wait add 100 useless seconds to the run.

Уточнення на співбесіді — «Що поверне findElement, якщо збігів три?» Перший у порядку документа; тому неунікальний локатор — прихована помилка, а не дрібниця.

Interview follow-up — "What does findElement return when three elements match?" The first one in document order, which is why a non-unique locator is a hidden bug rather than a detail.

Java + Selenium 4 — перевірка відсутності
Java + Selenium 4 — checking absence
// Гість не повинен бачити кнопку видалення
List<WebElement> buttons = driver.findElements(By.cssSelector("[data-testid='delete-post']"));
assertThat(buttons).isEmpty();

// findElement кидає виняток, якщо елемента немає
assertThrows(NoSuchElementException.class,
    () -> driver.findElement(By.id("delete-post")));

// Елемент має зникнути після дії — чекаємо явно
new WebDriverWait(driver, Duration.ofSeconds(5))
    .until(ExpectedConditions.invisibilityOfElementLocated(By.id("spinner")));
// A guest must not see the delete button
List<WebElement> buttons = driver.findElements(By.cssSelector("[data-testid='delete-post']"));
assertThat(buttons).isEmpty();

// findElement throws when the element is missing
assertThrows(NoSuchElementException.class,
    () -> driver.findElement(By.id("delete-post")));

// The element must disappear after an action — wait explicitly
new WebDriverWait(driver, Duration.ofSeconds(5))
    .until(ExpectedConditions.invisibilityOfElementLocated(By.id("spinner")));
Що в коді: порожній список від findElements — штатний результат, тому AssertJ перевіряє його без обробки винятків. assertThrows показує поведінку findElement, а WebDriverWait закриває сценарій «має зникнути».
What the code shows: an empty list from findElements is a normal result, so AssertJ checks it with no exception handling. assertThrows shows how findElement behaves, and WebDriverWait covers the "must disappear" case.
💡 Простими словами: «Немає зараз» — це findElements(...).isEmpty(), «має зникнути» — це явне очікування невидимості. І не забудь, що implicit wait робить кожну перевірку відсутності повільною.
💡 In plain words: "Not there right now" is findElements(...).isEmpty(); "must go away" is an explicit invisibility wait. And remember that an implicit wait makes every absence check slow.
🗣 Як читати вголос Я розрізняю їх так: одиничний пошук кидає виняток, пошук списку — ні. Тому коли мені треба довести, що гість не бачить кнопку адміністратора, я беру список і перевіряю, що він порожній, без жодних try/catch у тесті. Колись у нас такі перевірки тягнулися по десять секунд кожна, і виявилось, що винен глобальний implicit wait. Ми його вимкнули, і регресія стала швидшою на кілька хвилин.
🗣 How to say it out loud The way I tell them apart: the single lookup throws, the list lookup doesn't. So when I need to prove a guest can't see the admin button, I take the list and assert it's empty, with no try/catch in the test at all. At one point those checks took ten seconds each on our project, and the culprit turned out to be a global implicit wait. We switched it off and the regression run got several minutes faster.
71 Як сховати явні очікування у хелпер фреймворку, щоб у тестах не було Thread.sleep? How do you wrap explicit waits in a framework helper so tests never call Thread.sleep?
Локатори й очікування в коді фреймворку Locators and waits in framework code middle ▶

Що це — Явне очікування (WebDriverWait, що наслідує FluentWait) опитує умову з певним інтервалом, доки вона не поверне істину або не мине таймаут. Хелпер на кшталт класу Waits збирає типові умови в одному місці: видимість, клікабельність, кількість рядків, текст.

What it is — An explicit wait (WebDriverWait, which extends FluentWait) polls a condition at an interval until it returns something truthy or the timeout runs out. A helper such as a Waits class keeps the common conditions in one place: visibility, clickability, row count, text.

Навіщо — Thread.sleep(3000) завжди програє: на швидкому стенді марнує час, на повільному все одно падає. Умовне очікування завершується тієї ж миті, коли стан настав, а таймаут береться з конфігу, тож для CI його можна підняти без правок коду.

Why — Thread.sleep(3000) always loses: on a fast stand it wastes time, on a slow one it still fails. A conditional wait finishes the moment the state is reached, and the timeout comes from config, so it can be raised for CI without touching code.

Приклад — Після фільтрації таблиця перемальовується асинхронно. Умова-лямбда d -> d.findElements(rows).size() == 3 чекає саме бізнес-стану, а не абстрактної видимості. Для елементів, що перебудовуються React-ом, FluentWait з ignoring(StaleElementReferenceException.class) пропускає проміжні збої.

Example — After filtering, a table re-renders asynchronously. The lambda condition d -> d.findElements(rows).size() == 3 waits for the actual business state rather than generic visibility. For elements React keeps rebuilding, a FluentWait with ignoring(StaleElementReferenceException.class) skips the transient failures.

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

Pitfalls — Do not ignore every exception type — that hides genuine locator mistakes. Nor should you wait "just in case" before every action: the helper should wait for a specific condition tied to the next step.

Уточнення на співбесіді — «Який інтервал опитування за замовчуванням?» У WebDriverWait це 500 мс; я змінюю його через pollingEvery лише тоді, коли умова дорога або стан змінюється дуже швидко.

Interview follow-up — "What is the default polling interval?" For WebDriverWait it is 500 ms; I change it with pollingEvery only when the condition is expensive or the state flips very quickly.

Java — клас-хелпер очікувань
Java — a waits helper class
public final class Waits {
    private final WebDriverWait wait;

    public Waits(WebDriver driver, Duration timeout) { // таймаут із конфігу
        this.wait = new WebDriverWait(driver, timeout);
    }

    public WebElement visible(By locator) {
        return wait.until(ExpectedConditions.visibilityOfElementLocated(locator));
    }

    // Власна умова: у таблиці рівно expected рядків
    public void rowCount(By rows, int expected) {
        wait.until(d -> d.findElements(rows).size() == expected);
    }
}

// Для елементів, які часто перебудовуються
Wait<WebDriver> fluent = new FluentWait<>(driver)
    .withTimeout(Duration.ofSeconds(20))
    .pollingEvery(Duration.ofMillis(300))
    .ignoring(StaleElementReferenceException.class);
public final class Waits {
    private final WebDriverWait wait;

    public Waits(WebDriver driver, Duration timeout) { // timeout from config
        this.wait = new WebDriverWait(driver, timeout);
    }

    public WebElement visible(By locator) {
        return wait.until(ExpectedConditions.visibilityOfElementLocated(locator));
    }

    // Custom condition: the table has exactly expected rows
    public void rowCount(By rows, int expected) {
        wait.until(d -> d.findElements(rows).size() == expected);
    }
}

// For elements that get rebuilt often
Wait<WebDriver> fluent = new FluentWait<>(driver)
    .withTimeout(Duration.ofSeconds(20))
    .pollingEvery(Duration.ofMillis(300))
    .ignoring(StaleElementReferenceException.class);
Що в коді: until приймає будь-яку функцію від драйвера, тому лямбда з підрахунком рядків — повноцінна умова. FluentWait дає явний контроль над таймаутом, частотою опитування та списком винятків, які можна пропустити.
What the code shows: until accepts any function of the driver, so a row-counting lambda is a full condition. FluentWait gives explicit control over timeout, polling rate and the exceptions that may be skipped.
💡 Простими словами: Чекай на те, що побачив би користувач перед наступним кроком, а не на секунди. Якщо у pull request з'являється Thread.sleep, це привід для коментаря на рев'ю.
💡 In plain words: Wait for what a user would see before the next step, not for a number of seconds. A Thread.sleep showing up in a pull request is a reason for a review comment.
🗣 Як читати вголос У нашому фреймворку тести взагалі не створюють WebDriverWait самі — є один клас Waits, куди я виніс видимість, кількість рядків, появу тексту. Таймаут приходить з конфігу, тож у CI він більший, ніж локально. Коли стабілізував старий набір, найбільший виграш дало просто прибрати сорок з чимось sleep-ів і замінити їх умовами: прогін став і швидшим, і стабільнішим одночасно.
🗣 How to say it out loud In our framework tests never build a WebDriverWait themselves — there's one Waits class where I put visibility, row counts, text appearing and so on. The timeout comes from config, so it's longer in CI than locally. When I was stabilising an old suite, the biggest win was simply deleting forty-odd sleeps and replacing them with conditions: the run got faster and more stable at the same time.
72 Чому поєднання implicit і explicit wait — це баг у таймінгах, а не питання стилю? Why is combining implicit and explicit waits a timing bug rather than a matter of style?
Локатори й очікування в коді фреймворку Locators and waits in framework code middle ▶

Що це — Implicit wait — глобальне налаштування сесії: кожен findElement повторює пошук до заданого часу. Explicit wait опитує умову, і всередині цієї умови теж викликається findElement. Коли обидва активні, очікування вкладаються одне в одне.

What it is — An implicit wait is a session-wide setting: every findElement retries the lookup up to the given time. An explicit wait polls a condition, and that condition calls findElement too. With both active, the waits nest inside each other.

Навіщо це знати — Документація Selenium прямо попереджає: implicit 10 с і explicit 15 с можуть дати таймаут через 20 с. Реальна тривалість залежить від того, коли саме настав останній виклик пошуку, тому поведінка непередбачувана і відрізняється між браузерами та драйверами.

Why it matters — The Selenium documentation warns about exactly this: an implicit 10 s plus an explicit 15 s can time out after 20 s. The real duration depends on when the last lookup started, so the behaviour is unpredictable and varies between browsers and drivers.

Приклад — Очікування invisibilityOfElementLocated(spinner): спінер зник за пів секунди, але умова дізнається про це лише тоді, коли внутрішній findElement вичерпає 10 секунд implicit wait і кине NoSuchElementException. Тест проходить, але кожне таке очікування коштує 10 секунд.

Example — Waiting for invisibilityOfElementLocated(spinner): the spinner is gone after half a second, but the condition only learns that once the inner findElement exhausts the 10-second implicit wait and throws NoSuchElementException. The test passes, yet every such wait costs 10 seconds.

Підводні камені — Часто implicit wait виставляє хтось у базовому класі «для надійності», а інші автори про це не знають. Лікування: implicit дорівнює нулю в одному місці створення драйвера, усі очікування — явні через хелпер.

Pitfalls — Often someone sets the implicit wait in a base class "for reliability" and other authors never find out. The cure: implicit is zero in the single place where the driver is created, and every wait is explicit through a helper.

Уточнення на співбесіді — «А Selenide?» Він не використовує implicit wait: кожна перевірка на кшталт shouldBe(visible) — це власне явне очікування з Configuration.timeout.

Interview follow-up — "What about Selenide?" It does not rely on implicit waits: every check such as shouldBe(visible) is its own explicit wait governed by Configuration.timeout.

Java — як не треба і як треба
Java — the wrong way and the right way
// Погано: обидва механізми активні
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
new WebDriverWait(driver, Duration.ofSeconds(15))
    .until(ExpectedConditions.invisibilityOfElementLocated(By.id("spinner")));
// спінер зник за 0.5 с, а крок триває ~10 с

// Добре: implicit = 0 у фабриці драйвера, усе інше — явно
driver.manage().timeouts().implicitlyWait(Duration.ZERO);
waits.invisible(By.id("spinner"));
// Bad: both mechanisms are active
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
new WebDriverWait(driver, Duration.ofSeconds(15))
    .until(ExpectedConditions.invisibilityOfElementLocated(By.id("spinner")));
// the spinner left after 0.5 s, yet the step takes ~10 s

// Good: implicit = 0 in the driver factory, everything else explicit
driver.manage().timeouts().implicitlyWait(Duration.ZERO);
waits.invisible(By.id("spinner"));
Що в коді: перший блок відтворює вкладене очікування, через яке перевірка невидимості триває весь implicit-таймаут. Другий блок фіксує правило: нульовий implicit wait задається один раз, а очікування йдуть через хелпер waits.
What the code shows: the first block reproduces the nested wait that makes an invisibility check last the entire implicit timeout. The second block fixes the rule: a zero implicit wait is set once, and waits go through the waits helper.
💡 Простими словами: Обирай один механізм на весь проєкт, і хай це будуть явні очікування. Якщо тест «повільний без причини», першим ділом шукай implicitlyWait у коді.
💡 In plain words: Pick one mechanism for the whole project, and make it explicit waits. When a test is "slow for no reason", search the code for implicitlyWait first.
🗣 Як читати вголос Я поясню на реальному випадку: у нас кожне закриття модалки займало рівно десять секунд, хоча в браузері вона зникала миттєво. Причина — хтось поставив implicit wait у базовому класі, а перевірка невидимості всередині явного очікування чекала, поки пошук здасться. Відтоді правило просте: implicit wait нульовий, і я прямо кажу на співбесіді, що змішування — це не стиль, а непередбачувані таймаути.
🗣 How to say it out loud Let me explain with a real case: closing any modal took exactly ten seconds for us, although in the browser it vanished instantly. Someone had set an implicit wait in the base class, and the invisibility check inside an explicit wait kept waiting for the lookup to give up. Since then the rule is simple: the implicit wait is zero, and in interviews I say plainly that mixing them isn't style, it's unpredictable timeouts.
73 Коли в UI-тестах виправдані JavascriptExecutor і Actions, а коли це ознака проблеми? When are JavascriptExecutor and Actions justified in UI tests, and when are they a smell?
Локатори й очікування в коді фреймворку Locators and waits in framework code middle ▶

Що це — Actions будує низькорівневі жести через протокол WebDriver: наведення, подвійний клік, перетягування, натискання клавіш із модифікаторами. JavascriptExecutor.executeScript виконує довільний JS у сторінці й повертає результат у Java.

What it is — Actions builds low-level gestures through the WebDriver protocol: hover, double click, drag and drop, key presses with modifiers. JavascriptExecutor.executeScript runs arbitrary JS in the page and returns the result to Java.

Коли виправдано — Actions — це нормальний спосіб відтворити реальний жест: меню, що відкривається при наведенні, або drag-and-drop на канбан-дошці. JS доречний для службових речей: прокрутити елемент у центр, прочитати стан застосунку (кількість активних запитів), очистити localStorage перед тестом.

When it is justified — Actions is the proper way to reproduce a real gesture: a hover menu or drag and drop on a kanban board. JS fits utility jobs: scrolling an element to the centre, reading application state (the number of pending requests), clearing localStorage before a test.

Приклад запаху — Тест падав з ElementClickInterceptedException, і хтось замінив клік на arguments[0].click(). Тест позеленів, але в продакшені кнопку оплати перекривав банер cookies, і користувачі не могли на неї натиснути. JS-клік обійшов саме ту перевірку, заради якої тест існував.

Smell example — A test failed with ElementClickInterceptedException, and someone replaced the click with arguments[0].click(). The test went green, but in production the cookie banner covered the pay button and users could not press it. The JS click bypassed exactly the check the test existed for.

Підводні камені — JS не генерує справжні події миші й клавіатури, тому обробники на mousedown чи input можуть не спрацювати. Встановлення значення поля через JS залишає React-стан незмінним.

Pitfalls — JS does not generate real mouse and keyboard events, so handlers on mousedown or input may never fire. Setting a field value through JS leaves React state unchanged.

Уточнення на співбесіді — «Як ви обробляєте перехоплений клік?» Чекаю зникнення оверлею або закриваю банер кроком тесту, а JS-клік дозволяю лише з коментарем і посиланням на тікет.

Interview follow-up — "How do you handle an intercepted click?" I wait for the overlay to disappear or close the banner as a test step, and a JS click is allowed only with a comment and a ticket link.

Java — доречні та сумнівні виклики
Java — sensible and dubious calls
Actions actions = new Actions(driver);
// Доречно: реальний жест користувача
actions.moveToElement(accountMenu).click(logoutItem).perform();
actions.dragAndDrop(taskCard, doneColumn).perform();

JavascriptExecutor js = (JavascriptExecutor) driver;
// Доречно: службова прокрутка і читання стану
js.executeScript("arguments[0].scrollIntoView({block: 'center'});", table);
Long pending = (Long) js.executeScript("return window.pendingRequests || 0;");

// Запах: клікне навіть кнопку під модальним вікном
js.executeScript("arguments[0].click();", payButton);
Actions actions = new Actions(driver);
// Fine: a real user gesture
actions.moveToElement(accountMenu).click(logoutItem).perform();
actions.dragAndDrop(taskCard, doneColumn).perform();

JavascriptExecutor js = (JavascriptExecutor) driver;
// Fine: utility scrolling and reading state
js.executeScript("arguments[0].scrollIntoView({block: 'center'});", table);
Long pending = (Long) js.executeScript("return window.pendingRequests || 0;");

// Smell: clicks the button even under a modal
js.executeScript("arguments[0].click();", payButton);
Що в коді: ланцюжок Actions виконується лише після perform(). Числа з executeScript приходять у Java як Long, а arguments[0] — це переданий WebElement. Змінна window.pendingRequests — лічильник, який застосунок виставляє сам.
What the code shows: an Actions chain runs only after perform(). Numbers from executeScript arrive in Java as Long, and arguments[0] is the passed WebElement. window.pendingRequests is a counter the application exposes itself.
💡 Простими словами: Користувач не може натиснути те, що натискає JS. Тому JS — для підготовки й читання стану, а всі дії, які перевіряє тест, мають іти звичайним кліком або через Actions.
💡 In plain words: A user can't click what JS clicks. So JS is for setup and reading state, while every action the test verifies should go through a normal click or Actions.
🗣 Як читати вголос Для мене Actions — це нормальний інструмент, бо він імітує живий жест: навести на меню, перетягнути картку. А от JS-клік я сприймаю як червоний прапорець. У мене був випадок, коли такий клік приховав банер, який перекривав кнопку оплати, і баг дійшов до користувачів. Тепер на рев'ю я прошу пояснення до кожного executeScript, що змінює сторінку.
🗣 How to say it out loud For me Actions is a perfectly normal tool, because it imitates a live gesture: hovering a menu, dragging a card. A JS click, though, is a red flag. I had a case where one hid a banner covering the payment button, and the bug made it to users. Now on review I ask for a justification on every executeScript that changes the page.
74 Хто сьогодні керує бінарниками драйверів: WebDriverManager чи Selenium Manager? Who manages driver binaries today: WebDriverManager or Selenium Manager?
Локатори й очікування в коді фреймворку Locators and waits in framework code junior ▶

Що це — Selenium спілкується з браузером через окремий бінарник драйвера: chromedriver, geckodriver, msedgedriver. Його версія має відповідати версії браузера. Раніше шлях задавали через webdriver.chrome.driver, потім з'явилася бібліотека WebDriverManager (bonigarcia), а з Selenium 4.6 у сам Selenium вбудовано Selenium Manager.

What it is — Selenium talks to a browser through a separate driver binary: chromedriver, geckodriver, msedgedriver. Its version has to match the browser. The path used to be set via webdriver.chrome.driver, then the WebDriverManager library (bonigarcia) appeared, and since Selenium 4.6 Selenium Manager ships inside Selenium itself.

Навіщо — Selenium Manager спрацьовує автоматично, якщо драйвер не знайдено в PATH: визначає версію браузера, завантажує потрібний драйвер і кешує його в ~/.cache/selenium. Для нового проєкту окрема залежність зазвичай не потрібна; WebDriverManager лишається корисним у старих версіях Selenium або коли потрібні його додаткові можливості.

Why — Selenium Manager kicks in automatically when no driver is found on the PATH: it detects the browser version, downloads the matching driver and caches it in ~/.cache/selenium. A new project usually needs no extra dependency; WebDriverManager remains handy on older Selenium versions or when you want its extra features.

Приклад — Типова помилка — SessionNotCreatedException з повідомленням, що ця версія ChromeDriver підтримує лише певну версію Chrome. Це трапляється, коли в репозиторії закомічено старий драйвер або шлях жорстко прописано в конфігу.

Example — The classic error is SessionNotCreatedException saying this version of ChromeDriver only supports a specific Chrome version. It happens when an old driver is committed to the repo or its path is hard-coded in config.

Підводні камені — Автозавантаження в CI означає залежність від інтернету й неявні оновлення браузера. Стабільніше запускати тести в Docker-образі із зафіксованою версією браузера або на Selenium Grid, де драйвер уже в контейнері.

Pitfalls — Auto-download in CI means depending on the internet and on silent browser upgrades. It is more stable to run tests in a Docker image with a pinned browser version or on a Selenium Grid, where the driver already lives in the container.

Уточнення на співбесіді — «А з RemoteWebDriver?» Тоді локальний драйвер не потрібен зовсім: драйвер і браузер працюють у вузлі Grid, а тест лише надсилає команди на його URL.

Interview follow-up — "And with RemoteWebDriver?" Then no local driver is needed at all: the driver and browser run on the Grid node, and the test only sends commands to its URL.

Java — три способи отримати драйвер
Java — three ways to get a driver
// Selenium 4.6+: Selenium Manager знайде або завантажить chromedriver сам
WebDriver local = new ChromeDriver();

// Старіший підхід: бібліотека WebDriverManager
WebDriverManager.chromedriver().setup();
WebDriver legacy = new ChromeDriver();

// CI: драйвер і браузер уже в контейнері Grid
WebDriver remote = new RemoteWebDriver(
    URI.create("http://selenium-hub:4444").toURL(), new ChromeOptions());
// Selenium 4.6+: Selenium Manager finds or downloads chromedriver itself
WebDriver local = new ChromeDriver();

// Older approach: the WebDriverManager library
WebDriverManager.chromedriver().setup();
WebDriver legacy = new ChromeDriver();

// CI: the driver and browser already live in the Grid container
WebDriver remote = new RemoteWebDriver(
    URI.create("http://selenium-hub:4444").toURL(), new ChromeOptions());
Що в коді: у першому варіанті немає жодного рядка про драйвер — саме так працює вбудований Selenium Manager. setup() від WebDriverManager виставляє системну властивість зі шляхом, а RemoteWebDriver переносить усю відповідальність за бінарники на Grid.
What the code shows: the first variant has no driver line at all — that is how the built-in Selenium Manager works. WebDriverManager's setup() sets the system property with the path, and RemoteWebDriver moves all responsibility for binaries to the Grid.
💡 Простими словами: Не комітьте chromedriver у репозиторій. Локально хай працює Selenium Manager, а в CI фіксуйте версію браузера через Docker-образ.
💡 In plain words: Never commit chromedriver to the repo. Let Selenium Manager handle it locally, and pin the browser version in CI through a Docker image.
🗣 Як читати вголос Коротко скажу так: з Selenium 4.6 окремо керувати драйверами майже не треба, бо є вбудований Selenium Manager. На попередньому проєкті ми ще мали WebDriverManager, і я його прибрав під час оновлення Selenium. А в CI ми взагалі не покладаємося на автозавантаження: тести йдуть на Grid у контейнерах, де версія Chrome зафіксована, тому раптове оновлення браузера не ламає нічний прогін.
🗣 How to say it out loud I'd put it briefly: since Selenium 4.6 you barely need to manage drivers separately, because Selenium Manager is built in. On my previous project we still had WebDriverManager, and I removed it when we upgraded Selenium. In CI we don't rely on auto-download at all: tests run on a containerised Grid with a pinned Chrome version, so a surprise browser update can't break the nightly run.
▶ 8 8
75 Що означає ізоляція тестів і як її непомітно ламають у коді? What does test isolation mean, and how is it quietly broken in code?
Ізоляція тестів, дублери і тестові дані Test isolation, doubles and test data middle ▶

Що це — Ізольований тест сам готує потрібний стан, не залежить від порядку запуску і не залишає слідів для інших. Його можна запустити окремо, у будь-якій послідовності чи паралельно — і результат той самий.

What it is — An isolated test prepares the state it needs, does not depend on run order and leaves no traces for others. You can run it alone, in any sequence or in parallel, and the result is the same.

Навіщо — Без ізоляції одне падіння тягне за собою каскад червоних тестів, і реальну причину шукати довго. Паралельний запуск неможливий, а повторний прогін одного тесту в IDE не відтворює проблему з CI.

Why — Without isolation one failure drags a cascade of red tests after it, and finding the real cause takes ages. Parallel runs become impossible, and re-running a single test in the IDE does not reproduce the CI problem.

Як ламають — Статичне поле, яке один тест заповнює, а інший читає. Спільний тестовий користувач, у якого хтось змінив пароль. Рядки в БД, що лишилися після попереднього прогону. @TestMethodOrder, за яким тест оформлення замовлення тихо спирається на тест додавання в кошик. Сесія браузера, перевикористана між тестами.

How it breaks — A static field one test fills and another reads. A shared test user whose password someone changed. Database rows left from the previous run. A @TestMethodOrder that lets a checkout test quietly rely on the add-to-cart test. A browser session reused across tests.

Як виявити — Увімкнути випадковий порядок методів через junit-platform.properties, запускати кожен тест поодинці й повторно, у Playwright — --repeat-each. Тест, що падає лише в певній компанії, майже завжди має спільний стан.

How to detect it — Turn on random method order in junit-platform.properties, run each test alone and repeatedly, and in Playwright use --repeat-each. A test that fails only in certain company almost always shares state.

Уточнення на співбесіді — «А якщо створення даних дороге?» Тоді спільні незмінні дані створюю один раз на клас, а все, що тест змінює, — окремо для кожного тесту.

Interview follow-up — "What if creating data is expensive?" Then shared read-only data is created once per class, and anything a test mutates is created per test.

Java + JUnit5 — спільний стан і виправлення
Java + JUnit5 — shared state and the fix
// Погано: тести передають стан через static-поле
class CartOrderDependentTest {
    private static String cartId;
    @Test void addItem()  { cartId = api.createCart(); api.add(cartId, "SKU-1"); }
    @Test void checkout() { api.checkout(cartId); } // залежить від порядку
}

// Добре: кожен тест має власний кошик і прибирає за собою
class CartTest {
    private String cartId;
    @BeforeEach void newCart()  { cartId = api.createCart(); }
    @AfterEach  void cleanup()  { api.deleteCart(cartId); }

    @Test void checkoutSucceedsWithOneItem() {
        api.add(cartId, "SKU-1");
        assertThat(api.checkout(cartId).status()).isEqualTo("PAID");
    }
}

# junit-platform.properties — ловимо залежність від порядку
junit.jupiter.testmethod.order.default=org.junit.jupiter.api.MethodOrderer$Random
// Bad: tests pass state through a static field
class CartOrderDependentTest {
    private static String cartId;
    @Test void addItem()  { cartId = api.createCart(); api.add(cartId, "SKU-1"); }
    @Test void checkout() { api.checkout(cartId); } // depends on order
}

// Good: every test has its own cart and cleans up after itself
class CartTest {
    private String cartId;
    @BeforeEach void newCart()  { cartId = api.createCart(); }
    @AfterEach  void cleanup()  { api.deleteCart(cartId); }

    @Test void checkoutSucceedsWithOneItem() {
        api.add(cartId, "SKU-1");
        assertThat(api.checkout(cartId).status()).isEqualTo("PAID");
    }
}

# junit-platform.properties — catch order dependence
junit.jupiter.testmethod.order.default=org.junit.jupiter.api.MethodOrderer$Random
Що в коді: JUnit5 створює новий екземпляр класу для кожного тесту, тому нестатичне поле cartId вже ізольоване, а static ламає це. Властивість MethodOrderer$Random перемішує порядок методів і швидко показує приховані залежності.
What the code shows: JUnit5 creates a new class instance per test, so the non-static cartId field is already isolated, while static breaks that. The MethodOrderer$Random property shuffles method order and quickly exposes hidden dependencies.
💡 Простими словами: Перевірка на ізоляцію проста: чи пройде тест, якщо запустити тільки його, двічі поспіль і в іншому порядку? Якщо хоч одна відповідь «ні» — він не ізольований.
💡 In plain words: The isolation check is simple: will the test pass when run alone, twice in a row and in a different order? If any answer is "no", it isn't isolated.
🗣 Як читати вголос Для мене ізольований тест — той, який я можу запустити сам-один з IDE о третій ночі, і він пройде. На одному проєкті в нас було п'ятнадцять падінь щоночі, а причина одна: тест оформлення замовлення брав кошик зі статичного поля, яке заповнював інший тест. Я увімкнув випадковий порядок методів, знайшов ще кілька таких зв'язків, і після рефакторингу кожен тест почав створювати свої дані сам.
🗣 How to say it out loud To me an isolated test is one I can launch on its own from the IDE at three in the morning and it passes. On one project we had fifteen failures every night with a single cause: the checkout test took its cart from a static field another test filled. I turned on random method order, found a few more links like that, and after the refactor each test started creating its own data.
76 Як ізолювати тестові дані при паралельному запуску на спільному стенді? How do you keep test data isolated when tests run in parallel on a shared environment?
Ізоляція тестів, дублери і тестові дані Test isolation, doubles and test data senior ▶

Ситуація — Вісім воркерів CI і ще два розробники ганяють тести на одному staging. Якщо всі логіняться як qa@example.com і купують той самий товар зі складу в 5 одиниць, падіння випадкові: хтось розлогінив сесію, хтось вичерпав залишок.

Situation — Eight CI workers plus two developers run tests against one staging. If everyone logs in as qa@example.com and buys the same product with 5 units in stock, failures are random: someone logged the session out, someone drained the stock.

Підходи — Унікальні дані на тест: суфікс з UUID чи часової мітки в email, назві, SKU. Акаунт на воркер: worker-фікстура в Playwright або пул облікових записів у Java, який видає користувача потоку. Створення через API в підготовці та видалення в прибиранні. Префікс простору імен (at-), щоб нічне завдання чистило «сирітські» записи.

Approaches — Unique data per test: a UUID or timestamp suffix in the email, name, SKU. An account per worker: a worker fixture in Playwright or an account pool in Java that hands a user to each thread. Creation through the API in setup and deletion in teardown. A namespace prefix (at-) so a nightly job can clean up orphaned records.

Компроміс — Заздалегідь засіяні дані швидкі, але крихкі й «старіють». Дані на льоту повільніші й вимагають API для створення, зате відтворювані. Я комбіную: довідники (країни, валюти) — засіяні й лише для читання, а все, що тест змінює, створюється на льоту.

Trade-off — Pre-seeded data is fast but brittle and goes stale. On-the-fly data is slower and needs creation APIs, yet it is reproducible. I combine them: reference data (countries, currencies) is seeded and read-only, while anything a test mutates is created on the fly.

Підводні камені — Прибирання в afterEach не спрацює, якщо процес убили, тому потрібне і фонове очищення. Унікальні імена не рятують від глобальних лімітів — квоти запитів чи спільного складу.

Pitfalls — Teardown in afterEach does not run if the process is killed, so background cleanup is needed too. Unique names do not save you from global limits such as rate quotas or shared stock.

Уточнення на співбесіді — «А якщо API для створення немає?» Прошу розробників тестовий ендпоінт чи сид-скрипт, а до того — окремий пул засіяних записів на кожного воркера.

Interview follow-up — "What if there is no creation API?" I ask developers for a test endpoint or a seed script, and until then use a separate pool of seeded records per worker.

TypeScript + Playwright — акаунт на воркер і унікальний SKU
TypeScript + Playwright — an account per worker and a unique SKU
import { test as base } from '@playwright/test';

type Account = { email: string; password: string };

export const test = base.extend<{}, { account: Account }>({
  account: [async ({}, use, workerInfo) => {
    // Свій користувач на кожен воркер — жодних гонок за спільний акаунт
    const email = 'at-w' + workerInfo.workerIndex + '-' + Date.now() + '@example.com';
    const account = await createUserViaApi(email);
    await use(account);
    await deleteUserViaApi(account.email); // прибирання після воркера
  }, { scope: 'worker' }],
});

test('купівля товару з власним залишком', async ({ page, account }) => {
  const sku = 'at-' + crypto.randomUUID().slice(0, 8);
  await createProductViaApi({ sku, stock: 1 });
  // ... логін як account і купівля sku
});
import { test as base } from '@playwright/test';

type Account = { email: string; password: string };

export const test = base.extend<{}, { account: Account }>({
  account: [async ({}, use, workerInfo) => {
    // One user per worker — no races over a shared account
    const email = 'at-w' + workerInfo.workerIndex + '-' + Date.now() + '@example.com';
    const account = await createUserViaApi(email);
    await use(account);
    await deleteUserViaApi(account.email); // cleanup after the worker
  }, { scope: 'worker' }],
});

test('buys a product with its own stock', async ({ page, account }) => {
  const sku = 'at-' + crypto.randomUUID().slice(0, 8);
  await createProductViaApi({ sku, stock: 1 });
  // ... log in as account and buy sku
});
Що в коді: другий параметр типу в extend описує worker-фікстури, а { scope: 'worker' } створює акаунт один раз на процес-воркер. Префікс at- в email і SKU дозволяє фоновому завданню знайти й видалити все, що лишилося після аварійних прогонів.
What the code shows: the second type parameter of extend describes worker fixtures, and { scope: 'worker' } creates the account once per worker process. The at- prefix in the email and SKU lets a background job find and delete whatever crashed runs left behind.
💡 Простими словами: Спільне — лише те, що ніхто не змінює. Усе, що тест змінює, належить тільки йому і має впізнаваний префікс для прибирання.
💡 In plain words: Share only what nobody modifies. Everything a test changes belongs to that test alone and carries a recognisable prefix for cleanup.
🗣 Як читати вголос Коли ми перейшли з одного потоку на вісім, половина падінь виявилась боротьбою за один тестовий акаунт і один товар на складі. Я зробив так, що кожен воркер отримує власного користувача, створеного через API, а товари тест створює сам з унікальним SKU і префіксом. Для страховки є нічна задача, що видаляє все з цим префіксом старше доби. Довідники ми залишили засіяними, бо їх ніхто не змінює.
🗣 How to say it out loud When we went from one thread to eight, half the failures turned out to be a fight over a single test account and a single product in stock. I made every worker get its own user created through the API, and tests now create products themselves with a unique SKU and a prefix. As a safety net there's a nightly job deleting anything with that prefix older than a day. Reference data stayed seeded, since nobody changes it.
77 DRY чи DAMP: скільки абстракції доречно в тестовому коді? DRY or DAMP: how much abstraction is healthy in test code?
Ізоляція тестів, дублери і тестові дані Test isolation, doubles and test data middle ▶

Що це — DRY (Don't Repeat Yourself) вимагає не дублювати знання. DAMP (Descriptive And Meaningful Phrases) каже, що в тесті читабельність важливіша: тіло тесту має читатися як специфікація, навіть якщо кілька рядків повторюються в сусідніх тестах.

What it is — DRY (Don't Repeat Yourself) says do not duplicate knowledge. DAMP (Descriptive And Meaningful Phrases) says that in a test readability matters more: the body should read like a specification, even if a few lines repeat in neighbouring tests.

Де межа — DRY доречний в інфраструктурі: фабрики драйверів, API-клієнти, білдери даних, хелпери очікувань. У самому тесті мають бути видимі три речі: вхідні дані, які важливі для сценарію, дія та перевірка. Якщо, щоб зрозуміти падіння, треба відкрити три допоміжні методи, абстракція зайва.

Where the line is — DRY belongs in the infrastructure: driver factories, API clients, data builders, wait helpers. Inside the test three things must stay visible: the inputs that matter for the scenario, the action and the assertion. If understanding a failure means opening three helper methods, the abstraction has gone too far.

Приклад — Метод runScenario("gold", 3, true) прибрав дублювання з десяти тестів знижок, але тепер незрозуміло, що означає true і де перевірка. Після рефакторингу кожен тест будує клієнта й замовлення через білдер, викликає розрахунок і перевіряє суму прямо у своєму тілі.

Example — A runScenario("gold", 3, true) method removed duplication from ten discount tests, but now nobody knows what true means or where the assertion is. After the refactor each test builds the customer and order with a builder, calls the calculation and checks the total right in its own body.

Підводні камені — Перевірки, заховані в хелпері, дають незрозумілі повідомлення про падіння. Умовна логіка (if, цикли) у тесті — теж ознака того, що один метод намагається бути кількома тестами; тут краще параметризація.

Pitfalls — Assertions hidden in a helper produce confusing failure messages. Conditional logic (if, loops) inside a test is another sign that one method is trying to be several tests; parameterisation fits better.

Уточнення на співбесіді — «То копіпаст у тестах — це нормально?» Повтор кроків підготовки — так, якщо він говорить про сценарій; повтор технічних деталей, як-от побудови HTTP-запиту, — ні, його виношу в клієнт.

Interview follow-up — "So copy-paste in tests is fine?" Repeating setup steps is, when they describe the scenario; repeating technical details, such as building an HTTP request, is not, and I move that into a client.

Java — надто DRY проти DAMP
Java — too DRY versus DAMP
// Надто DRY: що перевіряється і чому — не видно
@Test void discount3() { runScenario("gold", 3, true); }

// DAMP: тіло читається як специфікація
@Test
void goldCustomerGetsTenPercentOffForThreeItems() {
    Customer gold = aCustomer().withTier(Tier.GOLD).build();
    Order order = anOrder().forCustomer(gold).withItems(3).pricedAtCents(10_000).build();

    Money total = checkout.total(order);

    assertThat(total).isEqualTo(Money.ofCents(9_000)); // 10% знижки
}
// Too DRY: what is checked and why is invisible
@Test void discount3() { runScenario("gold", 3, true); }

// DAMP: the body reads like a specification
@Test
void goldCustomerGetsTenPercentOffForThreeItems() {
    Customer gold = aCustomer().withTier(Tier.GOLD).build();
    Order order = anOrder().forCustomer(gold).withItems(3).pricedAtCents(10_000).build();

    Money total = checkout.total(order);

    assertThat(total).isEqualTo(Money.ofCents(9_000)); // 10% off
}
Що в коді: білдери aCustomer() і anOrder() — це DRY в інфраструктурі, але вони показують лише важливі для сценарію поля. Назва методу, дія і перевірка лишаються в тілі тесту, тому звіт про падіння зрозумілий без відкриття хелперів.
What the code shows: the aCustomer() and anOrder() builders are DRY in the infrastructure, yet they expose only the fields that matter to the scenario. The method name, the action and the assertion stay in the test body, so a failure report makes sense without opening helpers.
💡 Простими словами: Повторюйся в тестах, якщо повтор розповідає історію; не повторюйся в механіці. Хороший тест можна зрозуміти, не прокручуючи файл.
💡 In plain words: Repeat yourself in tests when the repetition tells the story; don't repeat mechanics. A good test can be understood without scrolling the file.
🗣 Як читати вголос Я прихильник DRY в інфраструктурі і DAMP у самих тестах. Колись я успадкував набір, де кожен тест був одним викликом універсального методу з п'ятьма булевими параметрами, і на розбір кожного падіння йшло пів години. Ми переписали їх так, щоб дані, дія і перевірка були видні в самому тесті, а повторювана механіка пішла в білдери. Тестів стало трохи довше, зате їх почали читати навіть розробники.
🗣 How to say it out loud I'm for DRY in the infrastructure and DAMP in the tests themselves. I once inherited a suite where every test was a single call to a universal method with five boolean flags, and each failure took half an hour to untangle. We rewrote them so data, action and assertion were visible in the test, and the repeated mechanics moved into builders. The tests got slightly longer, but even developers started reading them.
78 Що означають принципи FIRST і які з них найчастіше порушують UI-тести? What do the FIRST principles mean, and which of them do UI tests break most often?
Ізоляція тестів, дублери і тестові дані Test isolation, doubles and test data junior ▶

Що це — FIRST — абревіатура з книжок про чистий код: Fast (тест виконується за мілісекунди, тому його запускають постійно), Independent (не залежить від інших тестів і порядку), Repeatable (дає той самий результат на будь-якій машині й у будь-який час), Self-validating (сам каже pass або fail через assert, без ручного перегляду логів) і Timely (пишеться разом із кодом, а не через місяць після релізу).

What it is — FIRST is an acronym from the clean-code literature: Fast (a test runs in milliseconds, so people run it all the time), Independent (it does not rely on other tests or their order), Repeatable (same result on any machine at any time), Self-validating (it reports pass or fail through an assert, with no manual log reading) and Timely (written together with the code, not a month after the release).

Навіщо — принципи сформульовані для unit-тестів, але їх зручно використовувати як чекліст для будь-якого автотесту. Порушення кожної літери має свою ціну: повільні тести перестають запускати локально, залежні тести падають каскадом, неповторювані стають flaky.

Why — the principles were written for unit tests, yet they make a handy checklist for any automated test. Every broken letter has a cost: slow tests stop being run locally, dependent tests fail in cascades, non-repeatable ones turn flaky.

Приклад — UI і E2E-тести майже завжди порушують F (секунди або хвилини замість мілісекунд) і часто R (залежать від мережі, часу, стану стенду). Компенсуємо так: піднімаємо стан через API замість кліків, фіксуємо годинник і дані, запускаємо паралельно.

Example — UI and E2E tests almost always violate F (seconds or minutes instead of milliseconds) and often R (they depend on the network, the clock and the state of the environment). We compensate by preparing state through the API instead of clicks, pinning the clock and the data, and running in parallel.

Підводні камені — «Self-validating» ламається, коли тест лише друкує значення в консоль. «Timely» — коли автоматизація відстає від розробки на спринт і вже не ловить регресію вчасно.

Pitfalls — Self-validating breaks when a test only prints values to the console. Timely breaks when automation lags a sprint behind development and no longer catches the regression in time.

Уточнення на співбесіді — «Як перевірити Independent?» Запустити набір у випадковому порядку (MethodOrderer.Random) і кожен тест окремо.

Interview follow-up — "How do you check Independent?" Run the suite in random order (MethodOrderer.Random) and each test on its own.

Java — тест, що порушує R і S, та виправлена версія
Java — a test that breaks R and S, and the fixed version
// Погано: залежить від поточної дати і нічого не перевіряє
@Test
void discountBad() {
    var price = new PriceService().priceFor("SKU-1"); // усередині LocalDate.now()
    System.out.println(price); // людина має подивитись у лог
}

// Добре: фіксований годинник (Repeatable) і assert (Self-validating)
@Test
void blackFridayGivesTwentyPercent() {
    Clock clock = Clock.fixed(Instant.parse("2025-11-28T10:00:00Z"), ZoneOffset.UTC);
    var service = new PriceService(clock);
    assertThat(service.priceFor("SKU-1")).isEqualByComparingTo("80.00");
}
// Bad: depends on today's date and checks nothing
@Test
void discountBad() {
    var price = new PriceService().priceFor("SKU-1"); // uses LocalDate.now() inside
    System.out.println(price); // a human has to read the log
}

// Good: fixed clock (Repeatable) and an assert (Self-validating)
@Test
void blackFridayGivesTwentyPercent() {
    Clock clock = Clock.fixed(Instant.parse("2025-11-28T10:00:00Z"), ZoneOffset.UTC);
    var service = new PriceService(clock);
    assertThat(service.priceFor("SKU-1")).isEqualByComparingTo("80.00");
}
Що в коді: Clock.fixed прибирає залежність від реального часу, тож тест дає однаковий результат і в понеділок, і в Чорну п'ятницю. assertThat замінює друк у консоль на автоматичний вердикт.
What the code shows: Clock.fixed removes the dependency on real time, so the test gives the same result on a Monday and on Black Friday. assertThat replaces console printing with an automatic verdict.
💡 Простими словами: Якщо тест треба «дивитись очима», запускати в певному порядку або лише вранці — він порушує FIRST. Для UI-тестів чесно визнай, що F недосяжне, і компенсуй кількістю тестів на нижчих рівнях.
💡 In plain words: If a test has to be eyeballed, run in a specific order or only in the morning, it breaks FIRST. For UI tests, admit honestly that F is out of reach and compensate with more tests on the lower levels.
🗣 Як читати вголос Я сприймаю FIRST як швидкий чекліст, коли рев'юю тести колег. Найчастіше бачу проблему з повторюваністю: тест зав'язаний на сьогоднішню дату або на юзера, якого хтось інший змінив на стенді. У UI-тестах я не вдаю, що вони швидкі, — вони повільні за природою, тому я виношу підготовку даних в API і лишаю в браузері лише те, що реально треба клікати. А незалежність перевіряю банально: ганяю набір у випадковому порядку.
🗣 How to say it out loud I treat FIRST as a quick checklist when I review my colleagues' tests. The problem I see most is repeatability: a test tied to today's date, or to a user someone else modified on the environment. With UI tests I don't pretend they're fast, they're slow by nature, so I move data setup to the API and keep in the browser only what really needs clicking. And I check independence the boring way, by running the suite in random order.
79 Чим відрізняються dummy, stub, spy, mock і fake і коли який дублер обрати? How do dummy, stub, spy, mock and fake differ, and when do you pick each test double?
Ізоляція тестів, дублери і тестові дані Test isolation, doubles and test data middle ▶

Що це — «Test double» (термін Джерарда Мессароша) — будь-який об'єкт, що підміняє реальну залежність у тесті. Dummy лише заповнює параметр і ніколи не викликається. Stub повертає заздалегідь задані відповіді. Spy — stub, який ще й запам'ятовує, як його викликали. Mock наперед знає очікувані виклики і сам провалює тест, якщо їх не було. Fake — робоча, але спрощена реалізація: in-memory репозиторій, локальний SMTP.

What it is — A test double (Gerard Meszaros's term) is any object that replaces a real dependency in a test. A dummy only fills a parameter and is never called. A stub returns canned answers. A spy is a stub that also records how it was called. A mock knows the expected calls up front and fails the test itself if they did not happen. A fake is a working but simplified implementation: an in-memory repository, a local SMTP server.

Навіщо — дублери роблять тест швидким і детермінованим, дозволяють відтворити рідкісні відповіді (помилку платіжки) і перевірити побічні ефекти (лист надіслано).

Why — doubles make a test fast and deterministic, let you reproduce rare responses (a payment gateway error) and check side effects (an email was sent).

Приклад — сервіс реєстрації: логер у конструкторі — dummy; TaxClient з фіксованою ставкою — stub; поштовий сервіс, у якого перевіряємо один виклик send, — mock або spy; InMemoryUserRepository на HashMap — fake.

Example — a sign-up service: the logger in the constructor is a dummy; a TaxClient with a fixed rate is a stub; the mail service where we check one send call is a mock or a spy; an InMemoryUserRepository built on a HashMap is a fake.

Підводні камені — у Mockito усе називається mock(), тому люди плутають терміни; важлива не назва, а те, що саме перевіряє тест — стан чи взаємодію. Fake треба підтримувати, інакше він розходиться з реальною реалізацією.

Pitfalls — in Mockito everything is created with mock(), so people mix up the terms; what matters is not the name but what the test verifies, state or interaction. A fake needs maintenance, otherwise it drifts away from the real implementation.

Уточнення на співбесіді — «Що краще для репозиторію: mock чи fake?» Fake, якщо його використовують десятки тестів: він дає реальну поведінку збереження без налаштування кожного виклику.

Interview follow-up — "Which is better for a repository, a mock or a fake?" A fake if dozens of tests use it: it gives real save-and-load behaviour without stubbing every call.

Java — усі п'ять дублерів в одному тесті
Java — all five doubles in one test
@Test
void registersUserAndSendsWelcomeMail() {
    AuditLogger dummy = mock(AuditLogger.class);      // dummy: не використовується
    TaxClient stub = country -> new BigDecimal("0.20"); // stub: фіксована відповідь
    MailSender mail = mock(MailSender.class);          // mock: перевіримо виклик
    var repo = new InMemoryUserRepository();           // fake: HashMap усередині
    var clock = spy(Clock.systemUTC());               // spy: реальний об'єкт + запис викликів

    var service = new SignUpService(repo, stub, mail, dummy, clock);
    service.register("ann@test.io", "UA");

    assertThat(repo.findByEmail("ann@test.io")).isPresent(); // перевірка стану
    verify(mail).send(eq("ann@test.io"), contains("Welcome")); // перевірка взаємодії
}
@Test
void registersUserAndSendsWelcomeMail() {
    AuditLogger dummy = mock(AuditLogger.class);      // dummy: never used
    TaxClient stub = country -> new BigDecimal("0.20"); // stub: canned answer
    MailSender mail = mock(MailSender.class);          // mock: we verify the call
    var repo = new InMemoryUserRepository();           // fake: a HashMap inside
    var clock = spy(Clock.systemUTC());               // spy: real object + call recording

    var service = new SignUpService(repo, stub, mail, dummy, clock);
    service.register("ann@test.io", "UA");

    assertThat(repo.findByEmail("ann@test.io")).isPresent(); // state check
    verify(mail).send(eq("ann@test.io"), contains("Welcome")); // interaction check
}
Що в коді: stub зроблено звичайною лямбдою — для функціонального інтерфейсу Mockito не потрібен. spy() обгортає справжній об'єкт, а verify з матчерами eq і contains перевіряє аргументи виклику.
What the code shows: the stub is a plain lambda, since a functional interface needs no Mockito. spy() wraps a real object, and verify with the eq and contains matchers checks the call arguments.
💡 Простими словами: Stub відповідає на питання тесту, mock сам ставить питання коду. Якщо в тесті немає verify — у тебе stub, навіть коли він створений через mock().
💡 In plain words: A stub answers the test's questions, a mock asks the code its own. If there is no verify in the test, you have a stub, even when it was created with mock().
🗣 Як читати вголос Я пояснюю це через те, що саме тест перевіряє. Якщо мені просто треба, щоб залежність повернула курс валют, — це stub. Якщо важливо, що лист реально пішов, — це mock або spy з verify. А для репозиторію я люблю fake на HashMap: він поводиться як справжній, і не треба в кожному тесті розписувати when-thenReturn. Dummy згадую для повноти — це просто затичка для конструктора.
🗣 How to say it out loud I explain it through what the test is actually checking. If I just need a dependency to return an exchange rate, that's a stub. If it matters that the email really went out, that's a mock or a spy with verify. For a repository I like a fake built on a HashMap: it behaves like the real one and I don't have to write when-thenReturn in every test. A dummy I mention for completeness, it's just something to satisfy the constructor.
80 Як у Mockito перевірити стан і взаємодію: when/thenReturn, verify та ArgumentCaptor? How do you verify state and interaction in Mockito with when/thenReturn, verify and ArgumentCaptor?
Ізоляція тестів, дублери і тестові дані Test isolation, doubles and test data middle ▶

Що це — Mockito — найпоширеніша Java-бібліотека для дублерів. when(mock.call(args)).thenReturn(value) задає відповідь (стабінг), thenThrow — виняток. verify(mock).call(args) перевіряє, що виклик відбувся, з модифікаторами times(2), never(). ArgumentCaptor ловить переданий об'єкт, щоб перевірити його поля через AssertJ.

What it is — Mockito is the most widely used Java library for test doubles. when(mock.call(args)).thenReturn(value) defines an answer (stubbing), thenThrow defines an exception. verify(mock).call(args) checks the call happened, with modifiers like times(2) and never(). ArgumentCaptor catches the passed object so you can check its fields with AssertJ.

Навіщо — перевірка стану (що повернув метод, що лежить у репозиторії) стійкіша до рефакторингу. Перевірка взаємодії потрібна там, де результат — побічний ефект: подія в Kafka, лист, запит у платіжний шлюз.

Why — state verification (what the method returned, what is in the repository) survives refactoring better. Interaction verification is needed where the outcome is a side effect: an event in Kafka, an email, a call to a payment gateway.

Приклад — сервіс замовлень публікує OrderCreated. Ми стабимо склад, щоб товар був у наявності, викликаємо place(), а потім через captor дістаємо подію і перевіряємо суму та статус.

Example — an order service publishes OrderCreated. We stub the warehouse so the item is in stock, call place(), then use a captor to pull out the event and check its amount and status.

Підводні камені — надмірний verify на кожен внутрішній виклик цементує реалізацію: тест падає після безпечного рефакторингу. Не мокай типи, якими не володієш (HTTP-клієнт, JDBC) — обгорни їх власним інтерфейсом. MockitoExtension у strict-режимі падає на зайвих стабах (UnnecessaryStubbingException) — це корисний сигнал, а не привід вимикати перевірку.

Pitfalls — excessive verify on every internal call cements the implementation: the test fails after a safe refactoring. Do not mock types you do not own (an HTTP client, JDBC) — wrap them in your own interface. MockitoExtension in strict mode fails on unused stubs (UnnecessaryStubbingException), which is a useful signal rather than a reason to switch the check off.

Уточнення на співбесіді — «Чим doReturn().when() відрізняється від when().thenReturn()?» Перший не викликає реальний метод, тож потрібен для spy і void-методів.

Interview follow-up — "How does doReturn().when() differ from when().thenReturn()?" The first does not call the real method, so it is needed for spies and void methods.

Java — стабінг, verify і ArgumentCaptor
Java — stubbing, verify and ArgumentCaptor
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    @Mock Warehouse warehouse;
    @Mock EventPublisher events;
    @InjectMocks OrderService service;
    @Captor ArgumentCaptor<OrderCreated> eventCaptor;

    @Test
    void publishesEventWithTotal() {
        when(warehouse.inStock("SKU-7", 2)).thenReturn(true); // стабінг

        service.place("SKU-7", 2);

        verify(events, times(1)).publish(eventCaptor.capture()); // взаємодія
        assertThat(eventCaptor.getValue())
            .extracting(OrderCreated::total, OrderCreated::status)
            .containsExactly(new BigDecimal("39.98"), "NEW"); // поля події
        verifyNoMoreInteractions(events);
    }
}
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    @Mock Warehouse warehouse;
    @Mock EventPublisher events;
    @InjectMocks OrderService service;
    @Captor ArgumentCaptor<OrderCreated> eventCaptor;

    @Test
    void publishesEventWithTotal() {
        when(warehouse.inStock("SKU-7", 2)).thenReturn(true); // stubbing

        service.place("SKU-7", 2);

        verify(events, times(1)).publish(eventCaptor.capture()); // interaction
        assertThat(eventCaptor.getValue())
            .extracting(OrderCreated::total, OrderCreated::status)
            .containsExactly(new BigDecimal("39.98"), "NEW"); // event fields
        verifyNoMoreInteractions(events);
    }
}
Що в коді: @InjectMocks створює сервіс і підставляє в нього моки через конструктор. @Captor оголошує типізований captor без unchecked-попереджень, а verifyNoMoreInteractions гарантує, що зайвих подій не було.
What the code shows: @InjectMocks builds the service and injects the mocks through the constructor. @Captor declares a typed captor without unchecked warnings, and verifyNoMoreInteractions makes sure no extra events were published.
💡 Простими словами: Стаб — для входів, verify — для виходів, яких інакше не побачиш. Якщо результат можна перевірити через повернене значення, verify не потрібен.
💡 In plain words: Stubs are for inputs, verify is for outputs you could not otherwise see. If the result can be checked through the return value, you do not need verify.
🗣 Як читати вголос У моїх тестах правило просте: when-thenReturn я використовую, щоб задати поведінку залежностей, а verify — лише для побічних ефектів, які інакше не перевірити, наприклад, що подія пішла в чергу. Коли мені важливий вміст об'єкта, я беру ArgumentCaptor і далі звичайні AssertJ-перевірки. І я свідомо не мокаю сторонні клієнти напряму — обгортаю в свій інтерфейс, інакше тест ламається від оновлення бібліотеки.
🗣 How to say it out loud My rule in tests is simple: I use when-thenReturn to define how dependencies behave, and verify only for side effects I can't check otherwise, like an event going to a queue. When the content of the object matters, I grab it with an ArgumentCaptor and then use normal AssertJ checks. And I deliberately don't mock third-party clients directly; I wrap them in my own interface, otherwise a library upgrade breaks the tests.
81 Як за допомогою WireMock застабити сторонній API і змоделювати збої: таймаут, 503, обрив з'єднання? How do you stub a third-party API with WireMock and simulate failures such as a timeout, a 503 or a dropped connection?
Ізоляція тестів, дублери і тестові дані Test isolation, doubles and test data senior ▶

Що це — WireMock — HTTP-сервер-дублер: ти описуєш правила «запит → відповідь», а застосунок ходить на нього замість справжнього сервісу. Працює як JUnit5-розширення (@WireMockTest), як бібліотека в коді або як standalone-контейнер wiremock/wiremock поруч із застосунком у docker compose.

What it is — WireMock is a stand-in HTTP server: you describe "request → response" rules and the application calls it instead of the real service. It runs as a JUnit5 extension (@WireMockTest), as a library in code, or as the standalone wiremock/wiremock container next to the application in docker compose.

Коли — коли сторонній сервіс платний, нестабільний, не має тестового стенду або коли треба відтворити збої, яких справжній сервіс на замовлення не видасть: withFixedDelay(5000) для таймауту, статус 503 для ретраїв, Fault.CONNECTION_RESET_BY_PEER для обриву. Сценарії (inScenario) дають «спершу 503, потім 200», щоб перевірити логіку повторів.

When — when the third-party service is paid, unstable or has no test environment, or when you need failures the real service will not produce on demand: withFixedDelay(5000) for a timeout, status 503 for retries, Fault.CONNECTION_RESET_BY_PEER for a dropped connection. Scenarios (inScenario) give "first 503, then 200" to test retry logic.

Приклад — сервіс доставки питає тарифи в перевізника. Ми перевіряємо, що при таймауті понад 2 секунди сервіс віддає кешований тариф, а при 503 робить рівно три спроби — це видно через verify(3, getRequestedFor(...)).

Example — a delivery service asks a carrier for rates. We check that on a timeout over 2 seconds the service returns a cached rate, and that on 503 it makes exactly three attempts, visible through verify(3, getRequestedFor(...)).

Підводні камені — стаби застарівають: провайдер змінив поле, а тести зелені. Лікується контрактними тестами або періодичною звіркою стабів із записом реального трафіку. Не стаб власні сервіси команди без потреби — там краще контракт.

Pitfalls — stubs go stale: the provider renamed a field and the tests stay green. The cure is contract tests or periodically re-checking stubs against recorded real traffic. Do not stub your own team's services without need; a contract fits better there.

Уточнення на співбесіді — «Як направити застосунок на WireMock?» Базовий URL клієнта береться з конфігу, і в тесті підставляємо wmInfo.getHttpBaseUrl().

Interview follow-up — "How do you point the application at WireMock?" The client base URL comes from config, and in the test we pass wmInfo.getHttpBaseUrl().

Java — WireMock: успіх, затримка, обрив і перевірка кількості спроб
Java — WireMock: success, delay, dropped connection and attempt count
@WireMockTest
class CarrierClientTest {

    @Test
    void fallsBackToCacheOnTimeout(WireMockRuntimeInfo wm) {
        stubFor(get(urlPathEqualTo("/rates"))
            .willReturn(okJson("{\"price\": 120}").withFixedDelay(5000))); // довше за таймаут 2 с
        var client = new CarrierClient(wm.getHttpBaseUrl(), Duration.ofSeconds(2));

        assertThat(client.rate("KYIV").source()).isEqualTo("CACHE");
    }

    @Test
    void retriesThreeTimesOnConnectionReset(WireMockRuntimeInfo wm) {
        stubFor(get(urlPathEqualTo("/rates"))
            .willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER))); // обрив
        var client = new CarrierClient(wm.getHttpBaseUrl(), Duration.ofSeconds(2));

        assertThatThrownBy(() -> client.rate("KYIV")).isInstanceOf(CarrierUnavailable.class);
        verify(3, getRequestedFor(urlPathEqualTo("/rates"))); // рівно три спроби
    }
}
@WireMockTest
class CarrierClientTest {

    @Test
    void fallsBackToCacheOnTimeout(WireMockRuntimeInfo wm) {
        stubFor(get(urlPathEqualTo("/rates"))
            .willReturn(okJson("{\"price\": 120}").withFixedDelay(5000))); // longer than the 2 s timeout
        var client = new CarrierClient(wm.getHttpBaseUrl(), Duration.ofSeconds(2));

        assertThat(client.rate("KYIV").source()).isEqualTo("CACHE");
    }

    @Test
    void retriesThreeTimesOnConnectionReset(WireMockRuntimeInfo wm) {
        stubFor(get(urlPathEqualTo("/rates"))
            .willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER))); // dropped connection
        var client = new CarrierClient(wm.getHttpBaseUrl(), Duration.ofSeconds(2));

        assertThatThrownBy(() -> client.rate("KYIV")).isInstanceOf(CarrierUnavailable.class);
        verify(3, getRequestedFor(urlPathEqualTo("/rates"))); // exactly three attempts
    }
}
Що в коді: @WireMockTest піднімає сервер на випадковому порту і передає WireMockRuntimeInfo у тест. withFixedDelay і withFault моделюють збої, а verify(3, ...) рахує, скільки запитів реально дійшло до сервера.
What the code shows: @WireMockTest starts the server on a random port and passes WireMockRuntimeInfo into the test. withFixedDelay and withFault model failures, and verify(3, ...) counts how many requests actually reached the server.
💡 Простими словами: Найцінніше у WireMock — не «щасливі» стаби, а збої. Якщо в тебе немає жодного тесту на таймаут чи 503 від партнера, ти не знаєш, як застосунок поведеться в поганий день.
💡 In plain words: WireMock's biggest value is not happy-path stubs but failures. If you have no test for a partner timing out or returning 503, you do not know how the application behaves on a bad day.
🗣 Як читати вголос На попередньому проєкті ми інтегрувались з платіжним провайдером, у якого пісочниця падала через день. Я виніс його за WireMock і написав окремі тести на затримку, 503 і обрив з'єднання — і саме так ми знайшли, що ретраї робились нескінченно. Але я завжди кажу про ризик: стаб — це наша фантазія про чужий API, тому раз на спринт ми звіряли його з реальними відповідями.
🗣 How to say it out loud On my previous project we integrated with a payment provider whose sandbox went down every other day. I put it behind WireMock and wrote separate tests for a delay, a 503 and a dropped connection, and that's exactly how we found that retries were looping forever. But I always mention the risk: a stub is our fantasy about someone else's API, so once a sprint we checked it against real responses.
82 Коли Testcontainers з реальною PostgreSQL чи Kafka кращі за моки й H2 і скільки це коштує? When do Testcontainers with a real PostgreSQL or Kafka beat mocks and H2, and what do they cost?
Ізоляція тестів, дублери і тестові дані Test isolation, doubles and test data senior ▶

Що це — Testcontainers — бібліотека (Java, а також Node, Go, .NET), яка з тесту запускає Docker-контейнери з реальними залежностями: PostgreSQL, Kafka, Redis, LocalStack, навіть браузер. Контейнер стартує перед тестами на випадковому порту і знищується після них.

What it is — Testcontainers is a library (Java, plus Node, Go, .NET) that starts Docker containers with real dependencies from inside a test: PostgreSQL, Kafka, Redis, LocalStack, even a browser. A container starts before the tests on a random port and is destroyed after them.

Коли — коли поведінка залежить від конкретного рушія: JSONB, специфічні індекси, міграції Flyway, транзакційна ізоляція, партиції Kafka. H2 «в режимі PostgreSQL» пропускає такі баги, а мок репозиторію взагалі не перевіряє SQL. Інтеграційні тести на Testcontainers ловлять помилки міграцій і запитів ще до стенду.

When — when behaviour depends on a specific engine: JSONB, specific indexes, Flyway migrations, transaction isolation, Kafka partitions. H2 "in PostgreSQL mode" lets such bugs through, and a repository mock does not check SQL at all. Integration tests on Testcontainers catch migration and query errors before any shared environment.

Ціна — потрібен Docker на CI-агенті (або Testcontainers Cloud), старт PostgreSQL займає кілька секунд, Kafka — довше. Тому використовують патерн singleton-контейнера: один контейнер на весь JVM-запуск, стан чистять між тестами (truncate або транзакція з відкатом). Образи фіксують за тегом (postgres:16-alpine), а не latest.

Cost — the CI agent needs Docker (or Testcontainers Cloud), PostgreSQL takes a few seconds to start and Kafka longer. That is why teams use the singleton container pattern: one container per JVM run, with state cleaned between tests (truncate or a rolled-back transaction). Images are pinned by tag (postgres:16-alpine), not latest.

Підводні камені — @Container на нестатичному полі перезапускає контейнер для кожного тесту — дуже повільно. Без очищення даних тести стають залежними.

Pitfalls — @Container on a non-static field restarts the container for every test, which is very slow. Without data cleanup the tests become dependent on each other.

Уточнення на співбесіді — «А unit-тести тоді не потрібні?» Потрібні: бізнес-логіку дешевше перевіряти без бази, контейнер — для шару доступу до даних.

Interview follow-up — "So unit tests are no longer needed?" They are: business logic is cheaper to test without a database; the container is for the data access layer.

Java — PostgreSQL у контейнері з Spring Boot
Java — PostgreSQL in a container with Spring Boot
@SpringBootTest
@Testcontainers
class OrderRepositoryIT {

    // static: один контейнер на клас, а не на кожен тест
    @Container
    static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:16-alpine");

    @DynamicPropertySource
    static void props(DynamicPropertyRegistry r) {
        r.add("spring.datasource.url", db::getJdbcUrl); // випадковий порт
        r.add("spring.datasource.username", db::getUsername);
        r.add("spring.datasource.password", db::getPassword);
    }

    @Autowired OrderRepository repo;

    @Test
    void findsOrdersByJsonbTag() {
        repo.save(Order.withTags("{\"vip\": true}")); // JSONB, якого немає в H2
        assertThat(repo.findVip()).hasSize(1);
    }
}
@SpringBootTest
@Testcontainers
class OrderRepositoryIT {

    // static: one container per class, not per test
    @Container
    static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:16-alpine");

    @DynamicPropertySource
    static void props(DynamicPropertyRegistry r) {
        r.add("spring.datasource.url", db::getJdbcUrl); // random port
        r.add("spring.datasource.username", db::getUsername);
        r.add("spring.datasource.password", db::getPassword);
    }

    @Autowired OrderRepository repo;

    @Test
    void findsOrdersByJsonbTag() {
        repo.save(Order.withTags("{\"vip\": true}")); // JSONB, which H2 lacks
        assertThat(repo.findVip()).hasSize(1);
    }
}
Що в коді: @Testcontainers керує життєвим циклом полів з @Container. @DynamicPropertySource підставляє JDBC URL контейнера в Spring вже після старту, тому порт може бути будь-яким.
What the code shows: @Testcontainers manages the lifecycle of @Container fields. @DynamicPropertySource feeds the container's JDBC URL into Spring after startup, so the port can be anything.
💡 Простими словами: Тестуй SQL на тій самій базі, що й у продакшені. Якщо баг можливий лише в реальному рушії, ні H2, ні мок його не покажуть.
💡 In plain words: Test SQL on the same database you run in production. If a bug can only happen in the real engine, neither H2 nor a mock will show it.
🗣 Як читати вголос Ми перейшли з H2 на Testcontainers після інциденту: міграція з JSONB-індексом пройшла всі тести і впала на стенді. Тепер шар репозиторіїв тестується на справжній PostgreSQL тієї ж версії. Щоб CI не повільнішав, тримаємо один контейнер на весь запуск і чистимо таблиці між тестами. Я чесно кажу про мінус: потрібен Docker на агентах, і перший прогін довший, але це дешевше, ніж ловити такі речі на стенді.
🗣 How to say it out loud We moved from H2 to Testcontainers after an incident: a migration with a JSONB index passed every test and broke on the environment. Now the repository layer is tested on a real PostgreSQL of the same version. To keep CI fast we hold one container for the whole run and clean tables between tests. I'm honest about the downside: you need Docker on the agents and the first run is slower, but it's cheaper than catching this on a shared environment.
▶ 9 8
83 Які HTTP-методи і статус-коди ти перевіряєш в API-тестах і що ще варто перевірити, крім статусу? Which HTTP methods and status codes do you assert in API tests, and what else is worth checking besides the status?
API-тести в коді API testing in code junior ▶

Що це — GET читає ресурс, POST створює новий, PUT повністю замінює, PATCH змінює частину полів, DELETE видаляє. Для кожного методу є очікуваний «щасливий» статус: 200 для читання, 201 Created із заголовком Location для створення, 204 No Content для видалення без тіла.

What it is — GET reads a resource, POST creates a new one, PUT replaces it entirely, PATCH changes some fields, DELETE removes it. Each method has an expected happy status: 200 for reads, 201 Created with a Location header for creation, 204 No Content for a delete with no body.

Навіщо — негативні коди — окрема група перевірок: 400 — синтаксично поганий запит, 422 — валідний JSON, але порушено бізнес-правило (так роблять не всі API, тому дивимось контракт), 401 — немає або прострочений токен, 403 — токен є, але прав бракує, 404 — ресурсу немає, 409 — конфлікт (дублікат email), 5xx — помилка сервера, яка в тесті майже завжди баг.

Why — negative codes are a separate group of checks: 400 is a malformed request, 422 is valid JSON that breaks a business rule (not every API does this, so check the contract), 401 means a missing or expired token, 403 means a token without enough rights, 404 means no such resource, 409 is a conflict (a duplicate email), and 5xx is a server error that is almost always a bug in a test.

Приклад — після POST /users перевіряємо 201, Location, тіло (id, email), Content-Type, а потім GET за цим id, щоб переконатися, що ресурс справді збережено.

Example — after POST /users we check 201, Location, the body (id, email), Content-Type, and then a GET by that id to be sure the resource was really saved.

Підводні камені — перевірка лише статусу: API може повернути 200 з {"error": "..."} у тілі. Також плутають 401 і 403 — це різні баги безпеки.

Pitfalls — checking the status alone: an API can return 200 with {"error": "..."} in the body. People also confuse 401 and 403, which are different security bugs.

Уточнення на співбесіді — «Що перевірити у відповіді з помилкою?» Структуру помилки (код, повідомлення, поле) і те, що в ній немає stack trace.

Interview follow-up — "What do you check in an error response?" The error structure (code, message, field) and that it contains no stack trace.

Java + RestAssured — створення, читання і перевірка 401/403
Java + RestAssured — create, read and check 401/403
@Test
void createdUserCanBeReadBack() {
    String location = given().spec(adminSpec)
        .body(Map.of("email", "kate@test.io"))
    .when().post("/users")
    .then()
        .statusCode(201)                              // створено
        .header("Location", matchesPattern(".*/users/\\d+"))
        .body("email", equalTo("kate@test.io"))
        .extract().header("Location");

    given().spec(adminSpec).get(location).then().statusCode(200); // справді збережено
}

@Test
void deleteNeedsRights() {
    given().delete("/users/1").then().statusCode(401);                 // без токена
    given().spec(viewerSpec).delete("/users/1").then().statusCode(403); // токен без прав
}
@Test
void createdUserCanBeReadBack() {
    String location = given().spec(adminSpec)
        .body(Map.of("email", "kate@test.io"))
    .when().post("/users")
    .then()
        .statusCode(201)                              // created
        .header("Location", matchesPattern(".*/users/\\d+"))
        .body("email", equalTo("kate@test.io"))
        .extract().header("Location");

    given().spec(adminSpec).get(location).then().statusCode(200); // really saved
}

@Test
void deleteNeedsRights() {
    given().delete("/users/1").then().statusCode(401);                 // no token
    given().spec(viewerSpec).delete("/users/1").then().statusCode(403); // token without rights
}
Що в коді: extract().header() витягає Location, щоб наступним запитом перевірити створений ресурс. adminSpec і viewerSpec — заздалегідь налаштовані специфікації з різними токенами.
What the code shows: extract().header() pulls out Location so the next request can check the created resource. adminSpec and viewerSpec are pre-built specifications carrying different tokens.
💡 Простими словами: Статус — лише перший рядок перевірки. Після нього дивись тіло, ключові заголовки і те, що зміна дійсно сталася, окремим GET.
💡 In plain words: The status is only the first line of the check. After it, look at the body, the key headers and a separate GET proving the change really happened.
🗣 Як читати вголос Коли я пишу API-тест, статус — це мінімум. Для створення я чекаю 201, дивлюсь Location і потім роблю GET, бо бувало, що сервер відповідав успіхом, а запис не зберігався. На негативних сценаріях розділяю 401 і 403: перше — про автентифікацію, друге — про права, і це різні дефекти для розробника. І завжди перевіряю, що в помилці немає stack trace чи SQL.
🗣 How to say it out loud When I write an API test, the status is the bare minimum. For a create I expect 201, look at Location and then do a GET, because I've seen servers answer with success while the record never got saved. On negative scenarios I separate 401 and 403: the first is about authentication, the second about permissions, and they're different defects for a developer. And I always check that an error doesn't leak a stack trace or SQL.
84 Що таке ідемпотентність HTTP-методів і як перевірити її автотестом? What is idempotency of HTTP methods, and how do you check it with an automated test?
API-тести в коді API testing in code middle ▶

Що це — метод ідемпотентний, якщо повторний однаковий запит не змінює стан сервера понад те, що зробив перший. За RFC 9110 ідемпотентні GET, HEAD, PUT, DELETE, OPTIONS; POST і PATCH — ні. Важливо: ідемпотентність стосується стану, а не відповіді — другий DELETE може повернути 404, і це нормально.

What it is — a method is idempotent if repeating the same request does not change server state beyond what the first one did. Per RFC 9110, GET, HEAD, PUT, DELETE and OPTIONS are idempotent; POST and PATCH are not. Crucially, idempotency is about state, not the response: a second DELETE may return 404, and that is fine.

Навіщо — клієнти, балансувальники і мобільні застосунки повторюють запити після таймауту. Якщо платіжний POST повториться, людина заплатить двічі. Тому для критичних операцій API приймає заголовок Idempotency-Key: сервер запам'ятовує ключ і на повтор повертає той самий результат без нового списання.

Why — clients, load balancers and mobile apps retry requests after a timeout. If a payment POST is repeated, the customer pays twice. That is why APIs accept an Idempotency-Key header for critical operations: the server remembers the key and returns the same result for a repeat without charging again.

Приклад — тест 1: двічі шлемо однаковий PUT /profile і перевіряємо, що стан однаковий і версія не зросла двічі. Тест 2: два POST /payments з тим самим ключем повертають однаковий id, а в списку платежів лише один запис. Тест 3: той самий ключ з іншим тілом має дати помилку (часто 409 або 422).

Example — test 1: send the same PUT /profile twice and check the state is identical and the version did not grow twice. Test 2: two POST /payments with the same key return the same id, and the payment list holds one record. Test 3: the same key with a different body should give an error (often 409 or 422).

Підводні камені — паралельні повтори: послідовний тест проходить, а два одночасні запити створюють дубль. Перевір і конкурентний варіант.

Pitfalls — parallel repeats: the sequential test passes while two simultaneous requests create a duplicate. Check the concurrent variant too.

Уточнення на співбесіді — «Ідемпотентний і безпечний — це одне?» Ні: безпечний метод (GET) взагалі не змінює стан, а DELETE змінює, але повтор нічого не додає.

Interview follow-up — "Are idempotent and safe the same?" No: a safe method (GET) changes no state at all, while DELETE does change it, but a repeat adds nothing.

Java + RestAssured — повтор платежу з Idempotency-Key
Java + RestAssured — repeating a payment with Idempotency-Key
@Test
void samePaymentKeyChargesOnce() {
    String key = UUID.randomUUID().toString();
    var body = Map.of("orderId", orderId, "amount", 250);

    String first = pay(key, body).statusCode(201).extract().path("id");
    String second = pay(key, body).statusCode(anyOf(is(200), is(201))).extract().path("id");

    assertThat(second).isEqualTo(first);                          // той самий платіж
    assertThat(payments.listFor(orderId)).hasSize(1);            // без дубля в базі
}

private ValidatableResponse pay(String key, Object body) {
    return given().spec(api).header("Idempotency-Key", key).body(body)
        .post("/payments").then();
}
@Test
void samePaymentKeyChargesOnce() {
    String key = UUID.randomUUID().toString();
    var body = Map.of("orderId", orderId, "amount", 250);

    String first = pay(key, body).statusCode(201).extract().path("id");
    String second = pay(key, body).statusCode(anyOf(is(200), is(201))).extract().path("id");

    assertThat(second).isEqualTo(first);                          // the same payment
    assertThat(payments.listFor(orderId)).hasSize(1);            // no duplicate stored
}

private ValidatableResponse pay(String key, Object body) {
    return given().spec(api).header("Idempotency-Key", key).body(body)
        .post("/payments").then();
}
Що в коді: свіжий UUID гарантує, що ключ не перетнеться з іншими тестами. Другий виклик дозволяє 200 або 201, бо API по-різному сигналізують повтор, але id і кількість записів мають збігтися.
What the code shows: a fresh UUID ensures the key never collides with other tests. The second call allows 200 or 201, since APIs signal a replay differently, but the id and the record count must match.
💡 Простими словами: Ідемпотентність перевіряють по стану, а не по відповіді. Порахуй записи в базі чи списку після повтору — це і є справжній вердикт.
💡 In plain words: Idempotency is checked on state, not on the response. Count the records in the database or the list after the repeat; that is the real verdict.
🗣 Як читати вголос Я завжди уточнюю, що ідемпотентність — про стан сервера: другий DELETE може віддати 404, і це не баг. На практиці найважливіше це для платежів. У нас був тест, який двічі відправляв платіж з одним Idempotency-Key і рахував записи; пізніше я додав паралельну версію з двома потоками, і саме вона знайшла дубль, бо перевірка ключа на бекенді не була атомарною.
🗣 How to say it out loud I always point out that idempotency is about server state: a second DELETE can return 404 and that's not a bug. In practice it matters most for payments. We had a test that sent a payment twice with the same Idempotency-Key and counted the records; later I added a parallel version with two threads, and that's the one that found a duplicate, because the key check on the backend wasn't atomic.
85 Як побудувати шар API-клієнтів у фреймворку, щоб тести не збирали URL і заголовки вручну? How do you build an API client layer in the framework so tests never assemble URLs and headers by hand?
API-тести в коді API testing in code middle ▶

Що це — шар клієнтів — це набір класів на кшталт UsersClient, OrdersClient, де кожен метод відповідає одному ендпоінту: create(user), get(id). Базові налаштування (URL, токен, content type, логування, Allure-фільтр) живуть в одній RequestSpecification, а типові перевірки — у ResponseSpecification. У Playwright аналог — APIRequestContext з baseURL і extraHTTPHeaders.

What it is — a client layer is a set of classes like UsersClient and OrdersClient, where each method maps to one endpoint: create(user), get(id). Base settings (URL, token, content type, logging, the Allure filter) live in one RequestSpecification, and common checks in a ResponseSpecification. In Playwright the equivalent is an APIRequestContext with baseURL and extraHTTPHeaders.

Навіщо — коли ендпоінт змінюється з /users на /v2/users, правка одна, а не в сотні тестів. Тести читаються як бізнес-сценарій, а ті самі клієнти використовуються для підготовки даних в UI-тестах.

Why — when an endpoint moves from /users to /v2/users, you make one edit instead of touching a hundred tests. Tests read as a business scenario, and the same clients prepare data for UI tests.

Приклад — клієнт має два рівні методів: «сирий» повертає Response для негативних тестів, а «зручний» одразу перевіряє 201 і десеріалізує тіло в record — для підготовки даних.

Example — the client has two kinds of methods: a raw one returns a Response for negative tests, and a convenient one checks 201 and deserializes the body into a record straight away, for data setup.

Підводні камені — асерти бізнес-логіки всередині клієнта: тоді негативний тест неможливо написати. Статичний спільний стан у клієнті ламає паралельний запуск.

Pitfalls — business assertions inside the client, which make negative tests impossible to write. Static shared state inside a client breaks parallel runs.

Уточнення на співбесіді — «Чи генерувати клієнти з OpenAPI?» Можна, це прибирає ручну роботу, але згенерований код часто обгортають тонким шаром для читабельності тестів.

Interview follow-up — "Should clients be generated from OpenAPI?" They can be, it removes manual work, but generated code is often wrapped in a thin layer to keep tests readable.

Java + RestAssured — базова специфікація і клієнт ендпоінта
Java + RestAssured — base specification and an endpoint client
public class UsersClient {
    private final RequestSpecification spec;

    public UsersClient(Config cfg, String token) {
        this.spec = new RequestSpecBuilder()
            .setBaseUri(cfg.apiUrl())                 // URL лише з конфігу
            .setContentType(ContentType.JSON)
            .addHeader("Authorization", "Bearer " + token)
            .addFilter(new AllureRestAssured())       // запит/відповідь у звіт
            .build();
    }

    // «сирий» метод: для негативних тестів
    public Response createRaw(NewUser user) {
        return given().spec(spec).body(user).post("/users");
    }

    // зручний метод: для підготовки даних
    public User create(NewUser user) {
        return createRaw(user).then().statusCode(201).extract().as(User.class);
    }
}
public class UsersClient {
    private final RequestSpecification spec;

    public UsersClient(Config cfg, String token) {
        this.spec = new RequestSpecBuilder()
            .setBaseUri(cfg.apiUrl())                 // URL only from config
            .setContentType(ContentType.JSON)
            .addHeader("Authorization", "Bearer " + token)
            .addFilter(new AllureRestAssured())       // request/response in the report
            .build();
    }

    // raw method: for negative tests
    public Response createRaw(NewUser user) {
        return given().spec(spec).body(user).post("/users");
    }

    // convenient method: for data setup
    public User create(NewUser user) {
        return createRaw(user).then().statusCode(201).extract().as(User.class);
    }
}
Що в коді: RequestSpecBuilder збирає всі спільні налаштування один раз у конструкторі. Поле spec незмінне і належить екземпляру, тому кожен потік може мати власного клієнта з власним токеном.
What the code shows: RequestSpecBuilder gathers all shared settings once in the constructor. The spec field is final and per instance, so each thread can have its own client with its own token.
💡 Простими словами: Якщо в тесті є рядок з /api/v1/... — шар клієнтів протікає. Тест має говорити «створи користувача», а не «зроби POST на такий URL».
💡 In plain words: If a test contains a string with /api/v1/..., the client layer is leaking. A test should say "create a user", not "send a POST to this URL".
🗣 Як читати вголос У своєму фреймворку я тримаю по одному клієнту на ресурс і забороняю URL у тестах. Спільні речі — базовий адрес, токен, фільтр для Allure — зібрані в одній специфікації. Корисний трюк — два методи на ендпоінт: сирий для негативних перевірок і зручний, що одразу повертає об'єкт. Коли бекенд переїхав на другу версію API, ми змінили кілька рядків у клієнтах, і жоден тест не довелось чіпати.
🗣 How to say it out loud In my framework I keep one client per resource and URLs are banned in tests. Shared things, the base address, the token, the Allure filter, are gathered in one specification. A handy trick is two methods per endpoint: a raw one for negative checks and a convenient one that returns an object right away. When the backend moved to API version two, we changed a few lines in the clients and didn't have to touch a single test.
86 Як десеріалізувати API-відповідь у POJO чи record і перевірити її цілим об'єктом? How do you deserialize an API response into a POJO or record and assert it as a whole object?
API-тести в коді API testing in code middle ▶

Що це — Десеріалізація перетворює JSON-тіло відповіді на типізований об'єкт. У RestAssured це .extract().as(UserResponse.class), а під капотом працює Jackson (або Gson, якщо в classpath лише він). У Java 17 для моделей відповіді зручно брати record: він незмінний, має equals і читабельний toString, а Jackson з версії 2.12 створює record через канонічний конструктор без сеттерів.

What it is — Deserialization turns the JSON response body into a typed object. In RestAssured that is .extract().as(UserResponse.class), with Jackson doing the work underneath (or Gson if it is the only mapper on the classpath). On Java 17 a record is a handy response model: it is immutable, has equals and a readable toString, and since 2.12 Jackson builds records through the canonical constructor without setters.

Навіщо — Замість десятка body("address.city", equalTo(...)) з рядковими шляхами тест працює з полями, які перевіряє компілятор. Перейменували поле в моделі — проєкт не збереться, і ви дізнаєтесь про це до запуску, а не з падіння з незрозумілим JSON-шляхом.

Why — Instead of a dozen body("address.city", equalTo(...)) calls with string paths, the test works with fields the compiler checks. Rename a field in the model and the project stops compiling, so you learn about it before the run rather than from a failure pointing at an obscure JSON path.

Приклад — Створюємо користувача, забираємо відповідь як UserResponse і порівнюємо з очікуваним об'єктом через AssertJ usingRecursiveComparison().ignoringFields("id", "createdAt"). Поля, які генерує сервер, ігноруємо у загальному порівнянні й перевіряємо окремо, а все інше звіряємо одним виразом. У TypeScript as User нічого не перевіряє в рантаймі, тому тип доповнюють валідацією схемою.

Example — We create a user, extract the response as UserResponse and compare it with an expected object using AssertJ usingRecursiveComparison().ignoringFields("id", "createdAt"). Server-generated fields are left out of the bulk comparison and checked separately, and everything else is matched in one expression. In TypeScript as User checks nothing at runtime, so the type is paired with schema validation.

Підводні камені — Рішення щодо FAIL_ON_UNKNOWN_PROPERTIES: якщо увімкнено, кожне нове поле в API валить усі тести; якщо вимкнено, перейменоване поле тихо стане null. Робочий компроміс — вимкнути в моделях, а контроль структури віддати JSON Schema. Друга помилка — одна модель і для запиту, і для відповіді, хоча в запиті немає id і дат.

Pitfalls — The FAIL_ON_UNKNOWN_PROPERTIES decision: turned on, every new API field breaks all tests; turned off, a renamed field silently becomes null. A workable compromise is to switch it off in models and leave structure control to JSON Schema. The second mistake is sharing one model for request and response, even though the request has no id or timestamps.

Уточнення на співбесіді — «Чому не Map<String, Object>?» Map дає ті самі рядкові ключі без типів; для разової перевірки одного поля підходить, для моделі домену — ні.

Interview follow-up — "Why not a Map<String, Object>?" A map gives you the same string keys with no types; fine for a one-off field check, wrong for a domain model.

Java — record, RestAssured і рекурсивне порівняння AssertJ
Java — record, RestAssured and AssertJ recursive comparison
public record UserResponse(Long id, String email, String role, String createdAt) {}

@Test
void createdUserMatchesRequest() {
    // модель запиту — без серверних полів
    var request = new CreateUserRequest("qa@example.com", "EDITOR");

    UserResponse actual = given().spec(api)
        .body(request)
        .when().post("/users")
        .then().statusCode(201)
        .extract().as(UserResponse.class);

    var expected = new UserResponse(null, "qa@example.com", "EDITOR", null);
    // id і createdAt генерує сервер — перевіряємо окремо
    assertThat(actual)
        .usingRecursiveComparison()
        .ignoringFields("id", "createdAt")
        .isEqualTo(expected);
    assertThat(actual.id()).isPositive();
}
public record UserResponse(Long id, String email, String role, String createdAt) {}

@Test
void createdUserMatchesRequest() {
    // request model has no server-side fields
    var request = new CreateUserRequest("qa@example.com", "EDITOR");

    UserResponse actual = given().spec(api)
        .body(request)
        .when().post("/users")
        .then().statusCode(201)
        .extract().as(UserResponse.class);

    var expected = new UserResponse(null, "qa@example.com", "EDITOR", null);
    // id and createdAt come from the server, checked separately
    assertThat(actual)
        .usingRecursiveComparison()
        .ignoringFields("id", "createdAt")
        .isEqualTo(expected);
    assertThat(actual.id()).isPositive();
}
Що в коді: extract().as(...) віддає тіло мапперу Jackson; usingRecursiveComparison порівнює поля вглиб, а ignoringFields прибирає з порівняння значення, яких тест не може знати наперед.
What the code shows: extract().as(...) hands the body to the Jackson mapper; usingRecursiveComparison compares fields deeply, and ignoringFields drops values the test cannot know in advance.
💡 Простими словами: Окремі моделі для запиту й відповіді, record для відповіді, серверні поля — в ignoringFields і окремий assert. Так одне порівняння замінює десяток JSON-шляхів.
💡 In plain words: Separate request and response models, a record for the response, server-side fields in ignoringFields plus their own assert. One comparison then replaces a dozen JSON paths.
🗣 Як читати вголос Я майже не пишу перевірки через рядкові JSON-шляхи, хіба що для одного поля. Зазвичай у мене є record під відповідь, я витягую її через as і порівнюю з очікуваним об'єктом рекурсивно, виключаючи id і дати. Коли бекенд перейменовує поле, я бачу це ще на компіляції. А щоб не пропустити зміну структури, поруч тримаю перевірку схемою — модель і схема в мене відповідають за різне.
🗣 How to say it out loud I rarely write checks with string JSON paths, maybe for a single field. Usually I have a record for the response, I pull it out with as and compare it recursively against an expected object, excluding ids and dates. When the backend renames a field I find out at compile time. And so I don't miss a structural change, I keep a schema check next to it — for me the model and the schema have different jobs.
87 Що ловить валідація відповіді за JSON Schema, а що вона пропускає? What does JSON Schema validation of a response catch, and what does it miss?
API-тести в коді API testing in code middle ▶

Що це — JSON Schema — це JSON-документ, що описує форму відповіді: типи полів, обов'язкові поля (required), формати (email, date-time), переліки (enum), межі чисел і чи дозволені зайві поля (additionalProperties). У RestAssured перевірка підключається модулем json-schema-validator і матчером matchesJsonSchemaInClasspath, у TypeScript найчастіше беруть Ajv.

What it is — JSON Schema is a JSON document describing the shape of a response: field types, mandatory fields (required), formats (email, date-time), enumerations (enum), numeric limits and whether extra fields are allowed (additionalProperties). In RestAssured you add the json-schema-validator module and the matchesJsonSchemaInClasspath matcher; in TypeScript Ajv is the usual choice.

Навіщо — Схема ловить поломки контракту, які точкові перевірки пропускають: поле стало рядком замість числа, зник обов'язковий status, прийшов null, з'явилося значення поза enum. Одна схема на ендпоінт перевіряє всю структуру за один рядок і працює як дешевий захист від дрейфу API між релізами.

Why — A schema catches contract breakages that spot checks miss: a field turned from number into string, the mandatory status disappeared, a null arrived, a value outside the enum showed up. One schema per endpoint verifies the whole structure in one line and acts as a cheap guard against API drift between releases.

Приклад — Для GET /orders/{id} схема вимагає id як integer, status з enum NEW, PAID, SHIPPED і масив items мінімум з одним елементом. Файл лежить у src/test/resources/schemas, тест додає один матчер після statusCode(200).

Example — For GET /orders/{id} the schema requires id as an integer, status from the enum NEW, PAID, SHIPPED and an items array with at least one element. The file lives in src/test/resources/schemas, and the test adds one matcher after statusCode(200).

Підводні камені — Схема не знає бізнес-логіки: сума замовлення 0 замість 150, чужий userId чи неправильний статус після оплати пройдуть валідацію. Друга пастка — надто м'яка схема без required і з дозволеними зайвими полями, яка пропускає майже будь-що. Третя — валідатор підтримує конкретні драфти стандарту, тож ключове слово з новішого драфту може тихо ігноруватися.

Pitfalls — A schema knows no business logic: an order total of 0 instead of 150, someone else's userId or a wrong status after payment all pass validation. The second trap is an over-permissive schema with no required and extra fields allowed, which accepts almost anything. The third is that a validator supports specific drafts of the standard, so a keyword from a newer draft may be silently ignored.

Уточнення на співбесіді — «Звідки брати схему?» Найкраще генерувати з OpenAPI-специфікації сервісу, а не писати руками з однієї відповіді, інакше схема повторює випадкові дані.

Interview follow-up — "Where does the schema come from?" Ideally generated from the service's OpenAPI spec rather than hand-written from one response, otherwise it just mirrors accidental data.

Java — перевірка схемою в RestAssured і фрагмент schemas/order.json
Java — schema check in RestAssured and an excerpt of schemas/order.json
import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;

@Test
void orderResponseMatchesSchema() {
    given().spec(api)
        .when().get("/orders/{id}", orderId)
        .then()
        .statusCode(200)
        // структура: типи, required, enum
        .body(matchesJsonSchemaInClasspath("schemas/order.json"))
        // бізнес-значення схема не перевірить — окремий assert
        .body("total", equalTo(150.0f));
}
import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;

@Test
void orderResponseMatchesSchema() {
    given().spec(api)
        .when().get("/orders/{id}", orderId)
        .then()
        .statusCode(200)
        // structure: types, required, enum
        .body(matchesJsonSchemaInClasspath("schemas/order.json"))
        // the schema cannot check business values, so a separate assert
        .body("total", equalTo(150.0f));
}
{
  "type": "object",
  "required": ["id", "status", "items"],
  "additionalProperties": false,
  "properties": {
    "id": { "type": "integer" },
    "status": { "enum": ["NEW", "PAID", "SHIPPED"] },
    "total": { "type": "number", "minimum": 0 },
    "items": { "type": "array", "minItems": 1 }
  }
}
Що в коді: схема шукається в classpath, тобто в src/test/resources; additionalProperties: false робить її суворою, а перевірка total показує, що значення все одно треба звіряти окремо.
What the code shows: the schema is resolved from the classpath, i.e. src/test/resources; additionalProperties: false makes it strict, and the total check shows values still need their own assertion.
💡 Простими словами: Схема відповідає на питання «чи правильна форма», а не «чи правильні дані». Тримайте обидві перевірки поруч і генеруйте схему з OpenAPI.
💡 In plain words: A schema answers "is the shape right", not "is the data right". Keep both checks side by side and generate the schema from OpenAPI.
🗣 Як читати вголос Я розглядаю схему як дешеву страховку від того, що бекенд непомітно змінить тип поля чи прибере обов'язкове поле. На кожен ключовий ендпоінт у нас була схема, згенерована з OpenAPI, і один матчер у тесті. Але я завжди кажу, що схема зелена навіть тоді, коли сума в замовленні нульова, тому бізнес-значення перевіряю окремими assert. Ну і дивлюсь, щоб схема не була настільки м'якою, що пропускає все підряд.
🗣 How to say it out loud I treat a schema as cheap insurance against the backend quietly changing a field type or dropping a mandatory field. Every key endpoint in our project had a schema generated from OpenAPI and one matcher in the test. But I always point out that the schema stays green even when an order total is zero, so business values get their own asserts. And I make sure the schema isn't so loose that it lets everything through.
88 Як організувати автентифікацію в API-тестах, щоб не логінитися в кожному тесті? How do you handle authentication in API tests without logging in inside every test?
API-тести в коді API testing in code middle ▶

Що це — Більшість API захищені токеном: клієнт отримує access token (наприклад, OAuth2 client credentials або логін технічного користувача) і передає його в заголовку Authorization: Bearer .... У фреймворку за це відповідає окремий компонент — провайдер токена, який видає актуальний токен будь-якому клієнту.

What it is — Most APIs are protected by a token: the client obtains an access token (for example via OAuth2 client credentials or a technical user login) and sends it in the Authorization: Bearer ... header. In a framework this is owned by a separate component, a token provider that hands a valid token to any client.

Навіщо — Логін у кожному тесті множить час прогону і навантажує сервіс авторизації: 500 тестів — це 500 зайвих запитів, а деякі identity-провайдери ще й ставлять rate limit. Тому токен беруть один раз на прогін чи на воркер, кешують і оновлюють лише перед завершенням строку дії.

Why — Logging in inside every test multiplies run time and loads the auth service: 500 tests means 500 extra requests, and some identity providers also enforce rate limits. So the token is fetched once per run or per worker, cached, and refreshed only shortly before it expires.

Приклад — TokenProvider тримає токен і час завершення з поля expires_in, оновлюючи його за хвилину до кінця. Базова специфікація RestAssured додає заголовок через фільтр, тож кожен запит бере свіжий токен у момент відправлення. Для різних ролей (admin, viewer) — окремий токен на роль. У Playwright для UI аналогічну роль грає storageState, збережений у setup-проєкті.

Example — TokenProvider holds the token and its expiry from the expires_in field, refreshing a minute before it runs out. The base RestAssured specification adds the header through a filter, so every request picks up a fresh token at send time. Different roles (admin, viewer) get a token each. In Playwright UI tests the same job is done by a storageState saved in a setup project.

Підводні камені — Токен, захардкоджений у properties, протухає і валить нічний прогін; секрети клієнта мають іти з CI secrets, а не з репозиторію. Заголовок, записаний у специфікацію під час її створення, не оновиться після refresh. І найважливіше: сам логін, протермінований токен, чужа роль (403) перевіряються окремими тестами без кешованого токена.

Pitfalls — A token hardcoded in properties expires and breaks the nightly run; client secrets must come from CI secrets, not the repository. A header baked into the specification when it is built will not change after a refresh. And most importantly, the login itself, an expired token and a wrong role (403) are covered by dedicated tests that do not use the cached token.

Уточнення на співбесіді — «А при паралельному запуску?» Метод отримання токена синхронізований, тож потоки не штурмують auth-сервіс одночасно.

Interview follow-up — "What about parallel runs?" The fetch method is synchronized, so threads do not storm the auth service at the same moment.

Java — кешований токен і фільтр RestAssured
Java — cached token and a RestAssured filter
public final class TokenProvider {
    private static String token;
    private static Instant expiresAt = Instant.EPOCH;

    // один токен на прогін, оновлення за 60 с до кінця строку
    public static synchronized String get() {
        if (Instant.now().isAfter(expiresAt.minusSeconds(60))) {
            JsonPath json = given()
                .formParam("grant_type", "client_credentials")
                .formParam("client_id", Config.get("auth.clientId"))
                .formParam("client_secret", System.getenv("AUTH_SECRET"))
                .post(Config.get("auth.tokenUrl"))
                .then().statusCode(200)
                .extract().jsonPath();
            token = json.getString("access_token");
            expiresAt = Instant.now().plusSeconds(json.getLong("expires_in"));
        }
        return token;
    }
}

// фільтр підставляє актуальний токен у кожен запит
RequestSpecification api = new RequestSpecBuilder()
    .setBaseUri(Config.get("api.url"))
    .addFilter((req, res, ctx) -> {
        req.header("Authorization", "Bearer " + TokenProvider.get());
        return ctx.next(req, res);
    })
    .build();
public final class TokenProvider {
    private static String token;
    private static Instant expiresAt = Instant.EPOCH;

    // one token per run, refreshed 60 s before it expires
    public static synchronized String get() {
        if (Instant.now().isAfter(expiresAt.minusSeconds(60))) {
            JsonPath json = given()
                .formParam("grant_type", "client_credentials")
                .formParam("client_id", Config.get("auth.clientId"))
                .formParam("client_secret", System.getenv("AUTH_SECRET"))
                .post(Config.get("auth.tokenUrl"))
                .then().statusCode(200)
                .extract().jsonPath();
            token = json.getString("access_token");
            expiresAt = Instant.now().plusSeconds(json.getLong("expires_in"));
        }
        return token;
    }
}

// the filter puts the current token into every request
RequestSpecification api = new RequestSpecBuilder()
    .setBaseUri(Config.get("api.url"))
    .addFilter((req, res, ctx) -> {
        req.header("Authorization", "Bearer " + TokenProvider.get());
        return ctx.next(req, res);
    })
    .build();
Що в коді: synchronized не дає двом потокам одночасно просити новий токен; Filter у RestAssured — функціональний інтерфейс, тому лямбда додає заголовок у момент відправлення, а не при збиранні специфікації.
What the code shows: synchronized stops two threads from requesting a new token at once; RestAssured's Filter is a functional interface, so the lambda adds the header at send time rather than when the specification is built.
💡 Простими словами: Токен — це спільний ресурс фреймворку, а не крок тесту. Кешуйте його, оновлюйте за строком дії, секрети беріть із CI, а сам логін тестуйте окремо.
💡 In plain words: A token is a shared framework resource, not a test step. Cache it, refresh it by expiry, take secrets from CI, and test the login flow on its own.
🗣 Як читати вголос У нас спочатку кожен тест логінився сам, і на нічному прогоні auth-сервіс почав віддавати 429. Я виніс отримання токена в окремий провайдер: він кешує токен, дивиться на expires_in і оновлює його трохи заздалегідь. Заголовок підставляє фільтр, тому тести про авторизацію взагалі не думають. Окремо лишилося кілька тестів саме на логін, прострочений токен і 403 для не тієї ролі — вони йдуть без кешу.
🗣 How to say it out loud At first every test in our suite logged in by itself, and during the nightly run the auth service started returning 429. I moved token retrieval into a provider that caches it, looks at expires_in and refreshes a bit early. A filter adds the header, so tests don't think about auth at all. We kept a handful of tests specifically for login, an expired token and a 403 for the wrong role, and those run without the cache.
89 Як працює consumer-driven contract testing з Pact і де він стоїть у CI? How does consumer-driven contract testing with Pact work, and where does it sit in CI?
API-тести в коді API testing in code senior ▶

Що це — У consumer-driven підході контракт формує споживач. Тест споживача (фронтенд чи інший сервіс) ходить не в реальний провайдер, а в mock-сервер Pact і описує взаємодії: «на GET /users/42 чекаю 200 і тіло з полями id та email». Після тесту Pact записує ці очікування у pact-файл (JSON), який публікують у Pact Broker. Провайдер у своєму CI проганяє verification: Pact відтворює кожен запит проти реального сервісу й звіряє відповідь.

What it is — In the consumer-driven approach the consumer shapes the contract. The consumer test (a frontend or another service) calls a Pact mock server instead of the real provider and describes interactions: "for GET /users/42 I expect 200 and a body with id and email". Afterwards Pact writes these expectations to a pact file (JSON) that is published to a Pact Broker. The provider runs verification in its own CI: Pact replays every request against the real service and compares the response.

Коли це окупається — Коли сервісів багато і повний E2E-стенд повільний та нестабільний. Контракти дають швидку відповідь на питання «чи зламає мій реліз когось із клієнтів» без підняття всієї системи. Ворота — команда can-i-deploy: брокер перевіряє, чи ця версія сумісна з версіями, які вже стоять у цільовому середовищі.

When it pays off — When there are many services and a full E2E environment is slow and unstable. Contracts quickly answer "will my release break any of my clients" without bringing up the whole system. The gate is the can-i-deploy command: the broker checks whether this version is compatible with the versions already deployed to the target environment.

Приклад — Бекенд перейменував email на mail. Його unit і API-тести зелені, але verification пакту мобільного клієнта падає ще в PR, і can-i-deploy блокує деплой. Без контракту це знайшли б після релізу.

Example — The backend renamed email to mail. Its unit and API tests are green, but verification of the mobile client's pact fails right in the PR, and can-i-deploy blocks the deployment. Without the contract this would surface after the release.

Компроміси — Контракт перевіряє формат, а не бізнес-логіку провайдера. Потрібні provider states (підготовка даних «користувач 42 існує») і згода обох команд підтримувати процес. Для публічного API з невідомими клієнтами краще підходить provider-driven перевірка за OpenAPI. Типова помилка — пхати в пакт точні значення замість матчерів за типом, і тоді він ламається від будь-яких даних.

Trade-offs — A contract checks format, not the provider's business logic. You need provider states (data setup such as "user 42 exists") and both teams agreeing to maintain the process. For a public API with unknown clients, provider-driven checks against OpenAPI fit better. A classic mistake is putting exact values into the pact instead of type matchers, so it breaks on any data change.

Уточнення на співбесіді — «Чим це відрізняється від JSON Schema?» Схема описує все, що провайдер може віддати, а пакт — лише те, чим реально користується конкретний споживач.

Interview follow-up — "How is this different from JSON Schema?" A schema describes everything the provider may return; a pact covers only what a particular consumer actually uses.

GitHub Actions — публікація пакту і ворота can-i-deploy
GitHub Actions — publishing the pact and the can-i-deploy gate
# репозиторій споживача (web-app)
env:
  PACT_BROKER_BASE_URL: https://pact.example.com
  PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
jobs:
  contract:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # тести споживача проти mock-сервера Pact пишуть ./pacts/*.json
      - run: npm ci && npm run test:pact
      - name: Publish pacts
        run: >
          pact-broker publish ./pacts
          --consumer-app-version ${{ github.sha }}
          --branch ${{ github.ref_name }}
      # ворота перед деплоєм: чи сумісні ми з тим, що вже в prod
      - name: Can I deploy
        run: >
          pact-broker can-i-deploy
          --pacticipant web-app
          --version ${{ github.sha }}
          --to-environment production
# consumer repository (web-app)
env:
  PACT_BROKER_BASE_URL: https://pact.example.com
  PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
jobs:
  contract:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # consumer tests against the Pact mock server write ./pacts/*.json
      - run: npm ci && npm run test:pact
      - name: Publish pacts
        run: >
          pact-broker publish ./pacts
          --consumer-app-version ${{ github.sha }}
          --branch ${{ github.ref_name }}
      # gate before deploying: are we compatible with what is in prod
      - name: Can I deploy
        run: >
          pact-broker can-i-deploy
          --pacticipant web-app
          --version ${{ github.sha }}
          --to-environment production
Що в коді: версія пакту — це git SHA, тож брокер знає, яка саме збірка що очікує; CLI бере адресу й токен брокера зі змінних PACT_BROKER_BASE_URL і PACT_BROKER_TOKEN. Провайдер має свій job із verification і після деплою викликає record-deployment.
What the code shows: the pact version is the git SHA, so the broker knows exactly which build expects what; the CLI reads the broker address and token from PACT_BROKER_BASE_URL and PACT_BROKER_TOKEN. The provider has its own verification job and calls record-deployment after deploying.
💡 Простими словами: Пакт — це «список того, що мені від тебе треба», підписаний споживачем. Цінність дає не сам файл, а ворота can-i-deploy в обох пайплайнах.
💡 In plain words: A pact is the consumer's signed "list of what I need from you". The value is not the file itself but the can-i-deploy gate in both pipelines.
🗣 Як читати вголос Я пояснюю Pact так: споживач сам записує, що саме він чекає від API, і ця очікувана поведінка потім програється проти справжнього провайдера в його CI. Найкорисніше для мене — can-i-deploy, бо він відповідає на питання, чи можна викатити конкретну версію поруч із тим, що вже в продакшені. Але я чесно кажу, що це не заміна функціональних тестів провайдера і що без домовленості між командами контракти швидко перестають підтримуватись.
🗣 How to say it out loud The way I explain Pact: the consumer records what it actually expects from the API, and that expectation is replayed against the real provider in the provider's CI. The most useful part for me is can-i-deploy, because it tells you whether a specific version can go out next to what's already in production. But I'm upfront that it doesn't replace the provider's functional tests, and without an agreement between teams contracts stop being maintained pretty fast.
90 Як запускати Postman-колекцію в CI через Newman і коли час переносити її в код? How do you run a Postman collection in CI with Newman, and when is it time to move it into code?
API-тести в коді API testing in code junior ▶

Що це — Newman — консольний раннер Postman-колекцій. Колекцію експортують у JSON (або беруть через Postman API), оточення — окремим файлом зі змінними на кшталт baseUrl. Перевірки, написані у вкладці Tests через pm.test і pm.expect, Newman виконує так само, як Postman, і повертає ненульовий код виходу, якщо хоча б одна впала.

What it is — Newman is the command-line runner for Postman collections. The collection is exported to JSON (or fetched via the Postman API), and the environment is a separate file with variables such as baseUrl. Checks written in the Tests tab with pm.test and pm.expect run in Newman just as in Postman, and a non-zero exit code is returned if any of them fails.

Навіщо — Це найшвидший шлях зробити з ручних запитів регресійну перевірку в пайплайні: без нового фреймворку, силами тих, хто вже веде колекцію. Репортер junit дає XML, який CI показує як звичайні тести.

Why — It is the fastest way to turn manual requests into a regression check in the pipeline: no new framework, done by the people already maintaining the collection. The junit reporter produces XML that CI displays like ordinary tests.

Приклад — У job встановлюємо Newman, запускаємо колекцію з оточенням staging, секрет передаємо через --env-var із CI secrets, а звіт вивантажуємо як артефакт навіть при падінні.

Example — The job installs Newman, runs the collection with the staging environment, passes the secret with --env-var from CI secrets, and uploads the report as an artifact even when the run fails.

Підводні камені — Колекція в JSON погано рев'юється: diff на сотні рядків після зміни одного запиту. Логіка розмазана по pre-request скриптах, перевикористання обмежене, запити залежать від порядку через змінні колекції. Сигнали, що пора переходити в код: понад кілька десятків запитів, потрібні генерація даних, паралельний запуск, спільні клієнти з UI-тестами чи нормальне рев'ю в PR.

Pitfalls — A JSON collection is hard to review: changing one request yields a diff hundreds of lines long. Logic is scattered across pre-request scripts, reuse is limited, and requests depend on order through collection variables. Signals that it is time to move into code: more than a few dozen requests, a need for data generation, parallel runs, clients shared with UI tests, or proper PR review.

Уточнення на співбесіді — «Викинути колекцію після міграції?» Ні, її зручно лишити як живу документацію і пісочницю для ручного дослідження API.

Interview follow-up — "Throw the collection away after migrating?" No, it is still handy as living documentation and a sandbox for manual API exploration.

GitHub Actions — прогін колекції через Newman
GitHub Actions — running the collection with Newman
steps:
  - uses: actions/checkout@v4
  - run: npm install -g newman
  # колекція і оточення лежать у репозиторії поруч із тестами
  - name: API smoke
    run: >
      newman run postman/shop.postman_collection.json
      -e postman/staging.postman_environment.json
      --env-var apiKey=${{ secrets.API_KEY }}
      --reporters cli,junit
      --reporter-junit-export results/newman.xml
  # звіт потрібен саме тоді, коли прогін упав
  - if: always()
    uses: actions/upload-artifact@v4
    with:
      name: newman-report
      path: results/
steps:
  - uses: actions/checkout@v4
  - run: npm install -g newman
  # collection and environment live in the repo next to the tests
  - name: API smoke
    run: >
      newman run postman/shop.postman_collection.json
      -e postman/staging.postman_environment.json
      --env-var apiKey=${{ secrets.API_KEY }}
      --reporters cli,junit
      --reporter-junit-export results/newman.xml
  # the report matters most when the run failed
  - if: always()
    uses: actions/upload-artifact@v4
    with:
      name: newman-report
      path: results/
Що в коді: -e підключає файл оточення, --env-var перекриває одну змінну без комітування секрету, а if: always() вивантажує XML-звіт незалежно від результату попереднього кроку.
What the code shows: -e loads the environment file, --env-var overrides one variable without committing the secret, and if: always() uploads the XML report whatever the previous step's result.
💡 Простими словами: Newman — чудовий старт, але не фінал. Коли колекцію важко рев'юїти й вона тримається на порядку запитів, це сигнал переносити перевірки в код.
💡 In plain words: Newman is a great start, not the end state. When the collection is hard to review and relies on request order, that is the signal to move the checks into code.
🗣 Як читати вголос На одному проєкті API-перевірки жили в Postman, і перший крок, який я зробив, — запустив цю колекцію в CI через Newman з junit-репортером. За день у нас з'явився smoke після кожного деплою. Коли ж колекція виросла до сотні запитів і рев'ю її змін стало неможливим, ми поступово переписали критичні сценарії на RestAssured, а колекцію залишили для ручного дослідження.
🗣 How to say it out loud On one project the API checks lived in Postman, and the first thing I did was run that collection in CI through Newman with the junit reporter. Within a day we had a smoke check after every deploy. Once the collection grew to around a hundred requests and reviewing changes became impossible, we gradually rewrote the critical scenarios in RestAssured and kept the collection for manual exploration.
▶ 10 7
91 Як перетворити аналіз граничних значень на параметризований тест JUnit 5? How do you turn boundary value analysis into a JUnit 5 parameterized test?
Техніки тест-дизайну в коді Test design techniques in code junior ▶

Що це — Аналіз граничних значень (BVA) каже: помилки живуть на краях діапазонів, бо розробник пише > замість >=. Для діапазону 18–65 беремо значення на межі і поруч із нею: 17, 18, 19 та 64, 65, 66 (трьохточковий варіант) або лише 17, 18, 65, 66 (двоточковий). В автоматизації це ідеально лягає на @ParameterizedTest: одна логіка перевірки, багато рядків даних.

What it is — Boundary value analysis (BVA) says bugs live at the edges of ranges, because a developer writes > instead of >=. For a range of 18–65 we take values on and next to each boundary: 17, 18, 19 and 64, 65, 66 (three-point variant) or just 17, 18, 65, 66 (two-point). In automation this maps perfectly to @ParameterizedTest: one piece of check logic, many rows of data.

Навіщо — Шість окремих методів із копіпастою важко підтримувати, а таблиця в @CsvSource читається як тест-дизайн: вхід і очікуваний результат в одному рядку. Кожен рядок у звіті — окремий тест, тож видно, яка саме межа зламалась.

Why — Six copy-pasted methods are hard to maintain, while a table in @CsvSource reads like the test design itself: input and expected result on one line. Each row is a separate test in the report, so you see exactly which boundary broke.

Приклад — Правило «страховку можна оформити з 18 до 65 років включно». Атрибут name робить назви читабельними: «вік 17 → false». Якщо межа лежить у валідації API, той самий набір даних шлемо в запит і чекаємо 201 або 400.

Example — The rule is "insurance can be issued from age 18 to 65 inclusive". The name attribute makes names readable: "age 17 → false". If the boundary sits in API validation, the same data set goes into the request and we expect 201 or 400.

Підводні камені — Беруть лише «гарні» значення всередині діапазону й пропускають самі межі. Забувають про технічні межі типу: 0, від'ємне число, Integer.MAX_VALUE, порожній рядок, null (для рядків є @NullAndEmptySource). Ще одна помилка — очікуване значення обчислюють у тесті тією ж формулою, що й продакшн-код, і тест перевіряє сам себе.

Pitfalls — People pick only "nice" values inside the range and skip the boundaries themselves. They forget technical limits of the type: 0, a negative number, Integer.MAX_VALUE, an empty string, null (for strings there is @NullAndEmptySource). Another mistake is computing the expected value in the test with the same formula as the production code, so the test checks itself.

Уточнення на співбесіді — «А для дат?» Ті самі межі: останній день дії, перший прострочений, 29 лютого, перехід через північ у потрібній таймзоні.

Interview follow-up — "What about dates?" Same boundaries: the last valid day, the first expired one, 29 February, crossing midnight in the relevant time zone.

Java — межі 18..65 через @CsvSource
Java — the 18..65 boundaries via @CsvSource
@ParameterizedTest(name = "вік {0} -> дозволено: {1}")
@CsvSource({
    "17, false",  // нижче нижньої межі
    "18, true",   // нижня межа
    "19, true",
    "64, true",
    "65, true",   // верхня межа включно
    "66, false",  // вище верхньої межі
    "-1, false"   // технічна межа: від'ємний вік
})
void insuranceAgeBoundaries(int age, boolean allowed) {
    assertThat(policyService.canIssue(age)).isEqualTo(allowed);
}
@ParameterizedTest(name = "age {0} -> allowed: {1}")
@CsvSource({
    "17, false",  // below the lower boundary
    "18, true",   // lower boundary
    "19, true",
    "64, true",
    "65, true",   // upper boundary, inclusive
    "66, false",  // above the upper boundary
    "-1, false"   // technical limit: negative age
})
void insuranceAgeBoundaries(int age, boolean allowed) {
    assertThat(policyService.canIssue(age)).isEqualTo(allowed);
}
Що в коді: JUnit сам конвертує рядки CSV у int і boolean; {0} і {1} у name підставляють аргументи, тож у звіті видно конкретну межу, яка впала.
What the code shows: JUnit converts the CSV strings to int and boolean itself; {0} and {1} in name insert the arguments, so the report shows the exact boundary that failed.
💡 Простими словами: Кожна межа — це три рядки в таблиці: до, на і після. Очікуваний результат пишіть у тій самій таблиці літералом, а не обчислюйте.
💡 In plain words: Every boundary is three rows in the table: before, on and after. Write the expected result in the same table as a literal instead of computing it.
🗣 Як читати вголос Для мене BVA в коді — це майже завжди CsvSource. Я виписую межі з вимоги, додаю по значенню з кожного боку і кладу очікуваний результат у той самий рядок. Так бізнес-аналітик може подивитись на тест і сказати, що межу для 65 ми зрозуміли неправильно. А в звіті одразу видно, що впав саме рядок «66», а не абстрактний тест на вік.
🗣 How to say it out loud For me BVA in code almost always means CsvSource. I pull the boundaries from the requirement, add a value on each side and put the expected result on the same row. That way a business analyst can look at the test and tell me we misread the boundary at 65. And the report shows straight away that it was the "66" row that failed, not some vague age test.
92 Як закодувати класи еквівалентності в тестах і не дублювати перевірки? How do you encode equivalence partitions in tests without duplicating checks?
Техніки тест-дизайну в коді Test design techniques in code junior ▶

Що це — Еквівалентне розбиття ділить вхідні дані на класи, всередині яких система поводиться однаково. Якщо сервіс приймає оплату карткою VISA і Mastercard, а решту способів відхиляє, то класів два: «підтримувана картка» і «непідтримуваний спосіб». З кожного класу достатньо одного-двох представників — інші значення того ж класу не дадуть нової інформації.

What it is — Equivalence partitioning splits input data into classes within which the system behaves the same way. If a service accepts VISA and Mastercard payments and rejects every other method, there are two classes: "supported card" and "unsupported method". One or two representatives per class are enough; other values from the same class add no new information.

Навіщо — Техніка тримає набір тестів малим і осмисленим. У JUnit 5 класи природно лягають на джерела параметрів: @EnumSource з names для валідного класу і mode = EXCLUDE для решти, @ValueSource — для представників невалідних класів рядків.

Why — The technique keeps the test set small and meaningful. In JUnit 5 partitions map naturally onto argument sources: @EnumSource with names for the valid class and mode = EXCLUDE for the rest, @ValueSource for representatives of invalid string classes.

Приклад — Для email невалідні класи різні за природою: без «@», без домену, з пробілом, порожній. Кожен клас — один рядок, бо валідація відкидає їх з різних причин. Перелік EXCLUDE має приємний бонус: додали в enum новий спосіб оплати — він автоматично потрапив у тест «відхиляється», і якщо бізнес хотів його підтримати, тест про це нагадає.

Example — For email the invalid classes differ in nature: no "@", no domain, containing a space, empty. Each class gets one row because validation rejects them for different reasons. The EXCLUDE list has a nice bonus: add a new payment method to the enum and it automatically lands in the "rejected" test, so if the business meant to support it, the test will remind you.

Підводні камені — Двадцять значень з одного класу (a@b.com, c@d.com...) — це не покриття, а шум і довший прогін. Протилежна помилка — зливати різні невалідні класи в один тест, де перше ж невалідне поле маскує інші. Також класи визначає поведінка системи, а не зручність тестувальника: якщо для VIP-клієнтів інша логіка, це окремий клас.

Pitfalls — Twenty values from one class (a@b.com, c@d.com...) are noise and a longer run, not coverage. The opposite mistake is merging different invalid classes into one test, where the first invalid field masks the others. Also, classes are defined by system behaviour, not tester convenience: if VIP customers get different logic, that is a separate class.

Уточнення на співбесіді — «Як це поєднати з BVA?» Розбиття дає класи, а межі між числовими класами потім перевіряємо граничними значеннями.

Interview follow-up — "How does it combine with BVA?" Partitioning gives the classes, and the edges between numeric classes are then checked with boundary values.

Java — класи еквівалентності через @EnumSource і @ValueSource
Java — partitions via @EnumSource and @ValueSource
// валідний клас: підтримувані картки
@ParameterizedTest
@EnumSource(value = PaymentMethod.class, names = {"VISA", "MASTERCARD"})
void supportedMethodIsAccepted(PaymentMethod method) {
    assertThat(checkout.pay(order, method).status()).isEqualTo(Status.PAID);
}

// невалідний клас: усе інше, включно з новими значеннями enum
@ParameterizedTest
@EnumSource(value = PaymentMethod.class, mode = EnumSource.Mode.EXCLUDE,
            names = {"VISA", "MASTERCARD"})
void otherMethodsAreRejected(PaymentMethod method) {
    assertThatThrownBy(() -> checkout.pay(order, method))
        .isInstanceOf(UnsupportedPaymentException.class);
}

// по одному представнику на кожен невалідний клас email
@ParameterizedTest
@ValueSource(strings = {"user.example.com", "user@", "us er@example.com", ""})
void invalidEmailIsRejected(String email) {
    assertThat(validator.isValidEmail(email)).isFalse();
}
// valid class: supported cards
@ParameterizedTest
@EnumSource(value = PaymentMethod.class, names = {"VISA", "MASTERCARD"})
void supportedMethodIsAccepted(PaymentMethod method) {
    assertThat(checkout.pay(order, method).status()).isEqualTo(Status.PAID);
}

// invalid class: everything else, including new enum values
@ParameterizedTest
@EnumSource(value = PaymentMethod.class, mode = EnumSource.Mode.EXCLUDE,
            names = {"VISA", "MASTERCARD"})
void otherMethodsAreRejected(PaymentMethod method) {
    assertThatThrownBy(() -> checkout.pay(order, method))
        .isInstanceOf(UnsupportedPaymentException.class);
}

// one representative for each invalid email class
@ParameterizedTest
@ValueSource(strings = {"user.example.com", "user@", "us er@example.com", ""})
void invalidEmailIsRejected(String email) {
    assertThat(validator.isValidEmail(email)).isFalse();
}
Що в коді: names обирає значення enum для валідного класу, а Mode.EXCLUDE генерує всі інші; у @ValueSource кожен рядок — представник окремої причини відмови.
What the code shows: names selects the enum values for the valid class and Mode.EXCLUDE generates all the others; in @ValueSource each string represents a distinct reason for rejection.
💡 Простими словами: Питання до кожного нового рядка даних: «чи поводиться система тут інакше, ніж на вже наявних?». Якщо ні — рядок зайвий.
💡 In plain words: Ask of every new data row: "does the system behave differently here than for the rows I already have?" If not, the row is redundant.
🗣 Як читати вголос Коли я бачу тест із тридцятьма схожими email, я питаю, скільки там насправді різних причин відмови. Зазвичай їх чотири-п'ять, і стільки ж рядків достатньо. Для enum я люблю EnumSource з EXCLUDE: одного разу саме він спіймав, що новий спосіб оплати додали в код, але забули увімкнути на бекенді оплати. Тест впав першим, ще до того, як це помітили користувачі.
🗣 How to say it out loud When I see a test with thirty similar emails, I ask how many distinct rejection reasons there really are. Usually four or five, and that many rows is enough. For enums I like EnumSource with EXCLUDE: once it caught that a new payment method was added to the code but never enabled in the payment backend. The test failed first, before any user noticed.
93 Як автоматизувати тестування за таблицею рішень через @MethodSource? How do you automate decision table testing with @MethodSource?
Техніки тест-дизайну в коді Test design techniques in code middle ▶

Що це — Таблиця рішень описує правило з кількома умовами: кожен стовпчик — комбінація умов (член клубу, сума від 1000, є купон), а рядок дій — результат (знижка 0, 5, 10 чи 15%). Для трьох бінарних умов повна таблиця має 8 правил, частину з них бізнес зазвичай схлопує. У коді кожне правило стає одним набором аргументів.

What it is — A decision table describes a rule with several conditions: each column is a combination of conditions (club member, total of 1000 or more, has a coupon) and the action row is the outcome (0, 5, 10 or 15% discount). With three binary conditions the full table has 8 rules, some of which the business usually collapses. In code each rule becomes one set of arguments.

Навіщо — Саме комбінації умов найчастіше зламані: кожна умова окремо працює, а «член клубу з купоном» отримує подвійну знижку. @MethodSource повертає Stream<Arguments>, тож таблиця лежить у коді як є і рев'юється разом із вимогою.

Why — Combinations of conditions are what breaks most often: each condition works alone, yet "member with a coupon" gets a double discount. @MethodSource returns a Stream<Arguments>, so the table sits in the code as is and is reviewed together with the requirement.

Приклад — Правило: член клубу — 5%, сума від 1000 — ще 5%, купон — 10%, але купон не сумується з клубною знижкою, а максимум 15%. Вісім рядків arguments(...) повністю покривають правило, а назва тесту показує комбінацію. Якщо таблицю веде продакт, її виносять у CSV і читають через @CsvFileSource(resources = "/discount-rules.csv", numLinesToSkip = 1).

Example — The rule: club members get 5%, a total of 1000 or more adds another 5%, a coupon gives 10%, but the coupon does not stack with the club discount and the cap is 15%. Eight arguments(...) rows fully cover the rule, and the test name shows the combination. If a product owner maintains the table, it moves to a CSV read with @CsvFileSource(resources = "/discount-rules.csv", numLinesToSkip = 1).

Підводні камені — Писати лише «щасливі» правила й пропустити комбінацію, де умови конфліктують. Змішувати в одному методі кілька незалежних таблиць. Для 5–6 умов повна таблиця розростається до 32–64 рядків — тоді застосовують pairwise або схлопують правила з однаковим результатом, свідомо документуючи, що пропущено.

Pitfalls — Writing only the "happy" rules and skipping the combination where conditions conflict. Mixing several independent tables in one method. With 5–6 conditions a full table grows to 32–64 rows; then you use pairwise or collapse rules with the same result, deliberately documenting what was left out.

Уточнення на співбесіді — «Чому не @CsvSource?» Можна, але Arguments дає типізовані значення, enum і об'єкти, а не лише рядки.

Interview follow-up — "Why not @CsvSource?" You could, but Arguments gives typed values, enums and objects rather than just strings.

Java — таблиця рішень знижок через @MethodSource
Java — discount decision table via @MethodSource
//                член  сума≥1000  купон  -> знижка %
static Stream<Arguments> discountRules() {
    return Stream.of(
        arguments(false, false, false, 0),
        arguments(true,  false, false, 5),
        arguments(false, true,  false, 5),
        arguments(true,  true,  false, 10),
        arguments(false, false, true,  10),
        arguments(true,  false, true,  10),  // купон не сумується з клубною
        arguments(false, true,  true,  15),
        arguments(true,  true,  true,  15)   // стеля 15%
    );
}

@ParameterizedTest(name = "член={0}, велика сума={1}, купон={2} -> {3}%")
@MethodSource("discountRules")
void discountFollowsDecisionTable(boolean member, boolean bigOrder,
                                  boolean coupon, int expectedPercent) {
    var order = OrderBuilder.anOrder()
        .member(member).total(bigOrder ? 1200 : 300).coupon(coupon).build();
    assertThat(pricing.discountPercent(order)).isEqualTo(expectedPercent);
}
//                member total≥1000 coupon -> discount %
static Stream<Arguments> discountRules() {
    return Stream.of(
        arguments(false, false, false, 0),
        arguments(true,  false, false, 5),
        arguments(false, true,  false, 5),
        arguments(true,  true,  false, 10),
        arguments(false, false, true,  10),
        arguments(true,  false, true,  10),  // coupon does not stack with club discount
        arguments(false, true,  true,  15),
        arguments(true,  true,  true,  15)   // 15% cap
    );
}

@ParameterizedTest(name = "member={0}, big order={1}, coupon={2} -> {3}%")
@MethodSource("discountRules")
void discountFollowsDecisionTable(boolean member, boolean bigOrder,
                                  boolean coupon, int expectedPercent) {
    var order = OrderBuilder.anOrder()
        .member(member).total(bigOrder ? 1200 : 300).coupon(coupon).build();
    assertThat(pricing.discountPercent(order)).isEqualTo(expectedPercent);
}
Що в коді: статичний метод-фабрика повертає по одному Arguments на правило таблиці, @MethodSource посилається на нього за іменем, а коментар-заголовок над рядками робить код схожим на саму таблицю з вимоги.
What the code shows: the static factory method returns one Arguments per table rule, @MethodSource refers to it by name, and the header comment above the rows makes the code look like the table from the requirement.
💡 Простими словами: Один рядок arguments — одне правило з таблиці рішень. Якщо рядків більше, ніж правил у вимозі, або менше — спершу з'ясуйте, чого не вистачає у вимозі.
💡 In plain words: One arguments row is one rule from the decision table. If there are more or fewer rows than rules in the requirement, first find out what the requirement is missing.
🗣 Як читати вголос Таблиці рішень я витягую з вимог ще до того, як сідаю писати код, і часто вже на цьому етапі знаходжу дірку: ніхто не вирішив, що буде з купоном для члена клубу. Потім ця таблиця майже один в один переїжджає в MethodSource. Мені подобається, що на рев'ю аналітик читає рядки як свою ж таблицю. А коли комбінацій стає занадто багато, я переходжу на pairwise і записую, які саме рядки ми свідомо не перевіряємо.
🗣 How to say it out loud I pull decision tables out of the requirements before I start coding, and often I find a gap right there: nobody decided what happens to a coupon for a club member. Then the table moves almost one to one into a MethodSource. I like that in review the analyst reads the rows as their own table. And when there are too many combinations, I switch to pairwise and write down which rows we are deliberately not checking.
94 Як автоматизувати тестування переходів станів, включно з забороненими переходами? How do you automate state transition testing, including the forbidden transitions?
Техніки тест-дизайну в коді Test design techniques in code middle ▶

Що це — техніка, де об'єкт (замовлення, заявка, підписка) описують як скінченний автомат: стани, події та дозволені переходи між ними. Для замовлення це, наприклад, NEW → PAID → SHIPPED і гілка NEW/PAID → CANCELLED. Тести перевіряють кожен дозволений перехід і, що важливіше, кожну заборонену пару «стан + подія».

What it is — a technique where an entity (an order, an application, a subscription) is modelled as a finite state machine: states, events and the allowed transitions between them. For an order that is, say, NEW → PAID → SHIPPED plus a branch NEW/PAID → CANCELLED. Tests check every allowed transition and, more importantly, every forbidden "state + event" pair.

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

Why — the most expensive bugs in such domains live in forbidden transitions: cancelling an order that has already shipped, paying twice, shipping something unpaid. A manual tester usually walks the happy path, while a transition table forces you to consider every combination.

Приклад — я виписую таблицю «стан × подія» і перетворюю її на два параметризовані тести: валідні переходи очікують 200 і новий стан, невалідні — 409 Conflict і незмінний стан. Ключовий прийом — фабричний метод createInState(), який готує замовлення в потрібному стані через API або БД, а не проганяє весь ланцюжок подій перед кожною перевіркою.

Example — I write out a "state × event" table and turn it into two parameterized tests: valid transitions expect 200 and the new state, invalid ones expect 409 Conflict and an unchanged state. The key trick is a factory method createInState() that prepares the order in the required state via the API or the database instead of replaying the whole chain of events before each check.

Підводні камені — перевіряти лише код відповіді: сервер може повернути 409, але все одно змінити статус у базі. Друга помилка — покривати тільки 0-switch (окремі переходи) там, де баги виникають у послідовностях, наприклад «оплатив, скасував, оплатив знову».

Pitfalls — checking only the response code: the server can return 409 and still change the status in the database. The second mistake is covering only 0-switch (single transitions) where the bugs appear in sequences, such as "paid, cancelled, paid again".

Уточнення на співбесіді — «Скільки тестів це дає?» Для 4 станів і 3 подій — 12 комбінацій; я автоматизую всі, бо кожна — це один рядок CSV, а не новий метод.

Interview follow-up — "How many tests does that give?" With 4 states and 3 events there are 12 combinations; I automate all of them, because each one is a single CSV row, not a new method.

Java — таблиця переходів як параметри JUnit5
Java — the transition table as JUnit5 parameters
@ParameterizedTest(name = "{0} + {1} -> {2}")
@CsvSource({
    // стан, подія, очікуваний стан
    "NEW,  pay,    PAID",
    "PAID, ship,   SHIPPED",
    "NEW,  cancel, CANCELLED",
    "PAID, cancel, CANCELLED"
})
void validTransition(String from, String event, String expected) {
    long id = orders.createInState(from);   // готуємо стан напряму, без ланцюжка
    orders.apply(id, event).then().statusCode(200);
    assertThat(orders.get(id).state()).isEqualTo(expected);
}

@ParameterizedTest(name = "{0} + {1} is rejected")
@CsvSource({ "SHIPPED, cancel", "CANCELLED, pay", "NEW, ship" })
void forbiddenTransition(String from, String event) {
    long id = orders.createInState(from);
    orders.apply(id, event).then().statusCode(409);
    // статус у базі не мав змінитися
    assertThat(orders.get(id).state()).isEqualTo(from);
}
@ParameterizedTest(name = "{0} + {1} -> {2}")
@CsvSource({
    // state, event, expected state
    "NEW,  pay,    PAID",
    "PAID, ship,   SHIPPED",
    "NEW,  cancel, CANCELLED",
    "PAID, cancel, CANCELLED"
})
void validTransition(String from, String event, String expected) {
    long id = orders.createInState(from);   // set the state directly, no event chain
    orders.apply(id, event).then().statusCode(200);
    assertThat(orders.get(id).state()).isEqualTo(expected);
}

@ParameterizedTest(name = "{0} + {1} is rejected")
@CsvSource({ "SHIPPED, cancel", "CANCELLED, pay", "NEW, ship" })
void forbiddenTransition(String from, String event) {
    long id = orders.createInState(from);
    orders.apply(id, event).then().statusCode(409);
    // the stored status must not have changed
    assertThat(orders.get(id).state()).isEqualTo(from);
}
Що в коді: кожен рядок @CsvSource — клітинка таблиці переходів, а name робить її читабельною у звіті. Другий тест перевіряє не лише 409, а й те, що стан лишився старим.
What the code shows: each @CsvSource row is one cell of the transition table, and name makes it readable in the report. The second test checks not only the 409 but also that the state stayed the same.
💡 Простими словами: намалюй діаграму станів, порахуй усі пари «стан + подія» і зроби з них CSV. Заборонені пари — найцінніші рядки цієї таблиці.
💡 In plain words: draw the state diagram, count every "state + event" pair and turn them into CSV. The forbidden pairs are the most valuable rows in that table.
🗣 Як читати вголос На проєкті з замовленнями я спершу малював з аналітиком діаграму станів, бо в документації половини переходів просто не було. Потім переносив її в таблицю і робив два параметризовані тести: один для дозволених переходів, другий для заборонених. Найкорисніше виявилося саме друге — ми знайшли, що скасування відправленого замовлення повертало помилку, але гроші все одно поверталися клієнту. Тому я завжди перевіряю і відповідь, і фактичний стан після неї.
🗣 How to say it out loud On an order-management project I first drew the state diagram together with the analyst, because half of the transitions simply weren't documented. Then I moved it into a table and built two parameterized tests: one for allowed transitions and one for forbidden ones. The second turned out to be the useful one — we found that cancelling a shipped order returned an error but still refunded the customer. Since then I always check both the response and the actual state after it.
95 Чим відрізняється покриття рядків від покриття гілок і як виміряти його через JaCoCo? How does line coverage differ from branch coverage, and how do you measure it with JaCoCo?
Техніки тест-дизайну в коді Test design techniques in code middle ▶

Що це — покриття коду показує, яку частину продакшн-коду виконали тести. Покриття рядків (line) рахує виконані рядки, покриття гілок (branch) — чи пройдено обидва виходи кожного if, switch або тернарного оператора. Рядок if (total >= 1000 && vip) може бути «зеленим» по рядках і мати лише 50% по гілках.

What it is — code coverage shows what share of production code the tests executed. Line coverage counts executed lines; branch coverage checks whether both outcomes of every if, switch or ternary were taken. The line if (total >= 1000 && vip) can be "green" by lines and still sit at 50% by branches.

Навіщо — метрика знаходить непротестовані ділянки: гілки обробки помилок, рідкісні значення enum, мертвий код. Branch coverage корисніша, ніж line, бо саме в умовах ховаються граничні баги.

Why — the metric finds untested areas: error-handling branches, rare enum values, dead code. Branch coverage is more useful than line coverage, because boundary bugs hide precisely in conditions.

Приклад — у Maven підключаю jacoco-maven-plugin: ціль prepare-agent додає Java-агент у запуск Surefire, report генерує HTML і XML у target/site/jacoco, а check валить білд, якщо покриття гілок нижче порогу. Для API- чи E2E-тестів агент JaCoCo можна запустити прямо на тестовому сервері (-javaagent:jacocoagent.jar=output=tcpserver) і після прогону зібрати дамп через dump — так видно, що з бекенда реально зачепили наші автотести.

Example — in Maven I add jacoco-maven-plugin: the prepare-agent goal adds a Java agent to the Surefire run, report generates HTML and XML into target/site/jacoco, and check fails the build if branch coverage drops below a threshold. For API or E2E tests you can run the JaCoCo agent right on the test server (-javaagent:jacocoagent.jar=output=tcpserver) and collect a dump after the run with dump — that shows what our automation actually touched in the backend.

Підводні камені — покриття вимірює виконання, а не перевірку: тест без жодного assert дає ті самі відсотки. Також часто забувають виключити згенерований код (DTO, Lombok, мапери), і він роздуває цифру.

Pitfalls — coverage measures execution, not verification: a test with no assert yields the same percentage. People also forget to exclude generated code (DTOs, Lombok, mappers), which inflates the number.

Уточнення на співбесіді — «Який поріг ставити?» Я б не фіксував глобальну цифру, а вимагав покриття нового коду в PR, наприклад 80% гілок через SonarQube.

Interview follow-up — "What threshold would you set?" I wouldn't fix a global number; I'd require coverage on new code in the PR, for example 80% of branches via SonarQube.

pom.xml — JaCoCo з перевіркою покриття гілок
pom.xml — JaCoCo with a branch coverage check
<plugin>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <version>0.8.12</version>
  <executions>
    <!-- агент підключається до Surefire автоматично -->
    <execution><id>prepare</id><goals><goal>prepare-agent</goal></goals></execution>
    <execution><id>report</id><phase>verify</phase><goals><goal>report</goal></goals></execution>
    <execution>
      <id>check</id>
      <goals><goal>check</goal></goals>
      <configuration><rules><rule>
        <element>BUNDLE</element>
        <limits><limit>
          <!-- білд падає, якщо гілок покрито менше 70% -->
          <counter>BRANCH</counter>
          <value>COVEREDRATIO</value>
          <minimum>0.70</minimum>
        </limit></limits>
      </rule></rules></configuration>
    </execution>
  </executions>
</plugin>
<plugin>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <version>0.8.12</version>
  <executions>
    <!-- the agent is wired into Surefire automatically -->
    <execution><id>prepare</id><goals><goal>prepare-agent</goal></goals></execution>
    <execution><id>report</id><phase>verify</phase><goals><goal>report</goal></goals></execution>
    <execution>
      <id>check</id>
      <goals><goal>check</goal></goals>
      <configuration><rules><rule>
        <element>BUNDLE</element>
        <limits><limit>
          <!-- the build fails if fewer than 70% of branches are covered -->
          <counter>BRANCH</counter>
          <value>COVEREDRATIO</value>
          <minimum>0.70</minimum>
        </limit></limits>
      </rule></rules></configuration>
    </execution>
  </executions>
</plugin>
Що в коді: три execution — підготовка агента, звіт у фазі verify і перевірка порогу. counter BRANCH з COVEREDRATIO 0.70 означає «не менше 70% гілок по всьому модулю».
What the code shows: three executions — agent setup, a report in the verify phase, and a threshold check. counter BRANCH with COVEREDRATIO 0.70 means "at least 70% of branches across the whole module".
💡 Простими словами: покриття рядків відповідає на питання «чи заходили сюди тести», а покриття гілок — «чи пробували тести обидва варіанти умови». Дивись на друге.
💡 In plain words: line coverage answers "did the tests ever get here", branch coverage answers "did the tests try both sides of the condition". Look at the second one.
🗣 Як читати вголос Я розрізняю покриття рядків і гілок на простому прикладі: один тест з VIP-клієнтом зробить рядок з умовою зеленим, але випадок «не VIP» так і лишиться неперевіреним. Тому в JaCoCo я дивлюся саме на BRANCH. А ще мені подобається трюк з агентом на тестовому сервері: після нічного API-прогону ми бачили, які контролери наші тести взагалі не викликають, і це давало конкретний список того, що дописати.
🗣 How to say it out loud I explain line versus branch coverage with a simple case: one test with a VIP customer turns the condition line green, but the non-VIP case stays untested. That's why in JaCoCo I look at BRANCH. I also like the trick of running the agent on the test server: after the nightly API run we could see which controllers our tests never called at all, and that gave us a concrete list of what to write next.
96 Чому 100% покриття коду — погана ціль і що додає мутаційне тестування (PIT)? Why is 100% code coverage a poor goal, and what does mutation testing (PIT) add?
Техніки тест-дизайну в коді Test design techniques in code senior ▶

Що це — 100% покриття лише означає, що кожен рядок хоч раз виконався. Воно не гарантує, що тести щось перевіряють, і не бачить відсутнього коду: якщо розробник забув обробити від'ємну суму, покривати нема чого. Мутаційне тестування відповідає на інше питання: чи впадуть тести, якщо код зламати.

What it is — 100% coverage only means every line ran at least once. It does not guarantee the tests verify anything, and it cannot see missing code: if a developer forgot to handle a negative amount, there is nothing to cover. Mutation testing answers a different question: will the tests fail if the code is broken?

Коли і трейдоф — останні відсотки покриття найдорожчі: геттери, логування, неможливі catch. Команда під тиском цифри пише тести без assert, щоб «пофарбувати» рядки, і метрика перестає щось означати (закон Гудгарта). PIT (pitest-maven з pitest-junit5-plugin) створює мутанти — міняє >= на >, повертає null, прибирає виклик методу — і проганяє тести. Вижилий мутант означає слабку перевірку. Показник — mutation score, частка вбитих мутантів.

When and trade-off — the last percent of coverage is the most expensive: getters, logging, impossible catch blocks. Under pressure for the number, a team writes tests without asserts just to "paint" lines, and the metric stops meaning anything (Goodhart's law). PIT (pitest-maven with pitest-junit5-plugin) creates mutants — flips >= to >, returns null, removes a method call — and runs the tests. A surviving mutant means a weak check. The metric is the mutation score, the share of killed mutants.

Приклад — сервіс знижок мав 100% покриття, але PIT показав, що мутант >= 1000 → > 1000 вижив: жоден тест не перевіряв рівно 1000. Один граничний assert його вбив.

Example — a discount service had 100% coverage, but PIT showed that the mutant >= 1000 → > 1000 survived: no test checked exactly 1000. A single boundary assert killed it.

Підводні камені — PIT повільний, бо ганяє тести на кожен мутант, тож запускають його на змінених класах (targetClasses, інкрементальний аналіз) або вночі. Є «еквівалентні» мутанти, які неможливо вбити, — 100% mutation score теж не ціль.

Pitfalls — PIT is slow because it runs tests for every mutant, so you run it on changed classes (targetClasses, incremental analysis) or nightly. There are "equivalent" mutants that cannot be killed — a 100% mutation score is not a goal either.

Уточнення на співбесіді — «Чим замінити цифру?» Порогом на новий код у PR плюс mutation score для критичних модулів: платежі, тарифи, права доступу.

Interview follow-up — "What replaces the number?" A threshold on new code in the PR plus a mutation score for critical modules: payments, pricing, permissions.

Java — 100% покриття без користі і тест, що вбиває мутанта
Java — 100% coverage with no value, and a test that kills the mutant
// дає 100% рядків, але нічого не перевіряє
@Test
void calculatesDiscount() {
    service.discount(new Order(1000));
}

// PIT міняє ">= 1000" на "> 1000" — цей тест мутанта вбиває
@Test
void discountStartsExactlyAtThousand() {
    assertThat(service.discount(new Order(1000))).isEqualTo(100);
    assertThat(service.discount(new Order(999))).isZero();
}

// запуск лише для пакета знижок
// mvn org.pitest:pitest-maven:mutationCoverage -DtargetClasses=com.shop.discount.*
// gives 100% of lines but verifies nothing
@Test
void calculatesDiscount() {
    service.discount(new Order(1000));
}

// PIT flips ">= 1000" to "> 1000" — this test kills the mutant
@Test
void discountStartsExactlyAtThousand() {
    assertThat(service.discount(new Order(1000))).isEqualTo(100);
    assertThat(service.discount(new Order(999))).isZero();
}

// run only for the discount package
// mvn org.pitest:pitest-maven:mutationCoverage -DtargetClasses=com.shop.discount.*
Що в коді: обидва тести дають однакове покриття, але лише другий падає, коли PIT ламає умову. Параметр targetClasses звужує мутації до одного пакета, щоб прогін тривав хвилини, а не години.
What the code shows: both tests give the same coverage, but only the second fails when PIT breaks the condition. The targetClasses parameter narrows mutation to one package so the run takes minutes rather than hours.
💡 Простими словами: покриття каже, куди тести ходили; мутаційне тестування каже, чи вони там щось помітили б. Друге чесніше, але дорожче — використовуй його точково.
💡 In plain words: coverage says where the tests went; mutation testing says whether they would have noticed anything there. The second is more honest but costlier — use it selectively.
🗣 Як читати вголос Я бачив команду, якій менеджер поставив ціль «сто відсотків», і за місяць вони її досягли — переважно тестами без жодної перевірки. Тому на співбесіді я кажу, що цифра покриття корисна як ліхтарик, а не як KPI. Коли треба довести, що тести справді ловлять регресії, я запускаю PIT на критичному модулі й дивлюся на вижилих мутантів: кожен такий — конкретна дірка в assert'ах, яку легко показати розробникам.
🗣 How to say it out loud I've seen a team whose manager set a "one hundred percent" target, and within a month they hit it — mostly with tests that checked nothing. So in interviews I say the coverage number is useful as a flashlight, not as a KPI. When I need to prove the tests really catch regressions, I run PIT on a critical module and look at the surviving mutants: each one is a concrete gap in the assertions that's easy to show to the developers.
97 Як в автоматизації довести, які вимоги покриті тестами (трасованість)? How do you prove in automation which requirements are covered by tests (traceability)?
Техніки тест-дизайну в коді Test design techniques in code senior ▶

Що це — трасованість — це зв'язок «вимога ↔ тест ↔ результат прогону». В автоматизації його тримають у самому коді: ідентифікатор історії або вимоги стоїть поруч із тестом як анотація чи тег, а звіт прогону збирає ці мітки. Тоді матрицю покриття не ведуть вручну в Excel, а генерують з результатів CI.

What it is — traceability is the link "requirement ↔ test ↔ run result". In automation you keep it in the code itself: the story or requirement ID sits next to the test as an annotation or a tag, and the run report collects those labels. The coverage matrix is then generated from CI results instead of being maintained by hand in Excel.

Коли і трейдоф — це потрібно в регульованих доменах (медицина, фінанси), де аудитор питає «чим перевірено вимогу 4.2», і перед релізом, коли PO хоче знати, чи всі історії спринту мають зелені тести. Ціна — дисципліна: кожен новий тест має отримати мітку, а мітки треба оновлювати, коли вимоги змінюються.

When and trade-off — you need it in regulated domains (healthcare, finance), where an auditor asks "what verifies requirement 4.2", and before a release, when the PO wants to know whether every sprint story has green tests. The cost is discipline: every new test must get a label, and labels must be updated when requirements change.

Приклад — у Java я ставлю @TmsLink і @Tag("REQ-142"), у Playwright — tag і annotation в опціях тесту. Невеликий скрипт у CI бере список вимог спринту з Jira API, парсить allure-results або JUnit XML і будує таблицю: вимога — тести — статус. Вимоги без жодного тесту йдуть окремим списком «прогалини».

Example — in Java I put @TmsLink and @Tag("REQ-142"), in Playwright — tag and annotation in the test options. A small CI script pulls the sprint's requirements from the Jira API, parses allure-results or JUnit XML and builds a table: requirement — tests — status. Requirements with no test at all go into a separate "gaps" list.

Підводні камені — мітки «гниють»: вимогу закрили чи розбили на дві, а тест досі посилається на стару. Трасованість має бути двосторонньою — тест без вимоги теж сигнал (або мертвий тест, або незадокументована поведінка).

Pitfalls — labels rot: a requirement is closed or split in two, and the test still points to the old one. Traceability must be two-way — a test without a requirement is a signal too (either a dead test or undocumented behaviour).

Уточнення на співбесіді — «А якщо на одну вимогу 30 тестів?» Це нормально; матриця показує статус вимоги як найгірший статус її тестів.

Interview follow-up — "What if one requirement has 30 tests?" That's fine; the matrix shows the requirement's status as the worst status among its tests.

Java і TypeScript — мітки вимог у тестах
Java and TypeScript — requirement labels in tests
// Java: вимога як тег JUnit5 і посилання на тест-кейс у TMS
@Test
@Tag("REQ-142")
@TmsLink("TC-311")
@Story("Refund within 14 days")
void refundAllowedWithin14Days() { /* ... */ }

// Playwright: тег і анотація в опціях тесту
test('refund within 14 days', {
  tag: '@REQ-142',
  annotation: { type: 'requirement', description: 'REQ-142' },
}, async ({ request }) => { /* ... */ });

// запуск тестів лише однієї вимоги:
// mvn test -Dgroups=REQ-142   |   npx playwright test --grep @REQ-142
// Java: the requirement as a JUnit5 tag and a link to the TMS test case
@Test
@Tag("REQ-142")
@TmsLink("TC-311")
@Story("Refund within 14 days")
void refundAllowedWithin14Days() { /* ... */ }

// Playwright: a tag and an annotation in the test options
test('refund within 14 days', {
  tag: '@REQ-142',
  annotation: { type: 'requirement', description: 'REQ-142' },
}, async ({ request }) => { /* ... */ });

// run only the tests of one requirement:
// mvn test -Dgroups=REQ-142   |   npx playwright test --grep @REQ-142
Що в коді: ідентифікатор вимоги живе в тегу, тому його бачать і звіт, і фільтр запуску. @TmsLink перетворюється на клікабельне посилання в Allure, якщо в allure.properties задано шаблон.
What the code shows: the requirement ID lives in a tag, so both the report and the run filter can see it. @TmsLink becomes a clickable link in Allure once a pattern is set in allure.properties.
💡 Простими словами: не веди матрицю трасованості руками — поклади ID вимоги в тест, а таблицю нехай будує CI з результатів прогону.
💡 In plain words: don't maintain the traceability matrix by hand — put the requirement ID into the test and let CI build the table from the run results.
🗣 Як читати вголос На фінтех-проєкті перед кожним релізом комплаєнс просив доказ, що всі вимоги перевірені. Раніше це був Excel, який ніхто не оновлював. Я запропонував тегувати тести ID вимог і написав скрипт на пів сотні рядків, що звіряв Jira з allure-results. У підсумку ми отримали сторінку з трьома колонками і окремим списком вимог без тестів — і саме цей список виявився найкориснішим для планування.
🗣 How to say it out loud On a fintech project, compliance asked for proof before every release that all requirements had been verified. It used to be an Excel sheet nobody updated. I suggested tagging tests with requirement IDs and wrote a fifty-line script that matched Jira against allure-results. We ended up with a three-column page plus a separate list of requirements with no tests — and that list turned out to be the most useful thing for planning.
▶ 11 6
98 Яку стратегію роботи з flaky-тестами ти побудуєш: карантин, retry чи виправлення, які метрики? What strategy would you build for flaky tests: quarantine, retry or fix, and which metrics?
Flaky-тести, паралелізм і розбір падінь Flaky tests, parallelism and failure triage senior ▶

Що це — стратегія складається з чотирьох кроків: виявити, ізолювати, виправити, виміряти. Мета — щоб PR-гейт лишався довіреним сигналом: коли він червоний, люди мають шукати баг, а не тиснути «rerun».

What it is — the strategy has four steps: detect, isolate, fix, measure. The goal is to keep the PR gate a trusted signal: when it is red, people should look for a bug rather than press "rerun".

Коли і трейдоф — виявлення: тест, що на тому самому коміті то падає, то проходить, позначається як flaky (дані з історії Allure, статус «flaky» у Playwright або нічний прогін кожного тесту 20 разів). Ізоляція: тег quarantine прибирає тест з обов'язкового гейту, але він далі йде в нічному прогоні. Кожен карантин має власника, тікет і дедлайн, інакше карантин стає кладовищем. Виправлення за категоріями причин: синхронізація (відсутні очікування), дані (спільний користувач), порядок (залежність від іншого тесту), середовище (повільний стенд, нестабільний сторонній сервіс). Retry — лише тимчасова пов'язка, а не рішення.

When and trade-off — detection: a test that fails and passes on the same commit is marked flaky (Allure history data, Playwright's "flaky" status, or a nightly run of each test 20 times). Isolation: a quarantine tag removes the test from the required gate, but it still runs nightly. Every quarantined test has an owner, a ticket and a deadline, otherwise quarantine becomes a graveyard. Fixing goes by root-cause category: synchronisation (missing waits), data (a shared user), order (depends on another test), environment (slow stand, unstable third-party service). Retry is only a temporary bandage, not a solution.

Приклад — метрики: flaky rate (частка прогонів з нестабільними падіннями, ціль менше 1–2%), кількість тестів у карантині, середній час до виправлення, частка «зелених після rerun» у PR.

Example — metrics: flaky rate (share of runs with unstable failures, target under 1–2%), number of tests in quarantine, mean time to fix, share of PRs that went "green after rerun".

Підводні камені — карантинити тест, який насправді ловить гонку в продукті, і так приховати реальний баг. Тому перед карантином хтось дивиться trace і логи.

Pitfalls — quarantining a test that actually catches a race condition in the product and thereby hiding a real bug. So before quarantine someone looks at the trace and logs.

Уточнення на співбесіді — «Коли тест видаляти?» Якщо він місяць у карантині, ніхто не взявся і сценарій покритий на нижчому рівні — видаляю свідомо, з записом у тікеті.

Interview follow-up — "When do you delete a test?" If it has sat in quarantine for a month, nobody picked it up and the scenario is covered at a lower level — I delete it deliberately, with a note in the ticket.

Java + GitHub Actions — карантин поза PR-гейтом
Java + GitHub Actions — quarantine outside the PR gate
// QA-512, власник: @olena, дедлайн 2026-10-15
@Tag("quarantine")
@Test
void checkoutShowsDeliveryDate() { /* ... */ }

# .github/workflows/tests.yml
jobs:
  pr-gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # обов'язкова перевірка — без карантину
      - run: mvn -B test -DexcludedGroups=quarantine
  quarantine-nightly:
    if: github.event_name == 'schedule'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # карантин ганяємо вночі, збираємо історію
      - run: mvn -B test -Dgroups=quarantine
// QA-512, owner: @olena, deadline 2026-10-15
@Tag("quarantine")
@Test
void checkoutShowsDeliveryDate() { /* ... */ }

# .github/workflows/tests.yml
jobs:
  pr-gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # required check — quarantine excluded
      - run: mvn -B test -DexcludedGroups=quarantine
  quarantine-nightly:
    if: github.event_name == 'schedule'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # quarantine runs nightly to build up history
      - run: mvn -B test -Dgroups=quarantine
Що в коді: Surefire розуміє JUnit5-теги через groups і excludedGroups. Коментар над тестом — мінімальний «паспорт» карантину: тікет, власник, дедлайн.
What the code shows: Surefire understands JUnit5 tags through groups and excludedGroups. The comment above the test is the minimal quarantine "passport": ticket, owner, deadline.
💡 Простими словами: карантин без власника і дедлайну — це просто тихе видалення тесту. Завжди прив'язуй його до тікета і стеж за кількістю тестів у карантині.
💡 In plain words: quarantine without an owner and a deadline is just a quiet deletion of the test. Always tie it to a ticket and watch how many tests sit in quarantine.
🗣 Як читати вголос Коли я прийшов на проєкт, розробники натискали rerun у середньому двічі на PR і вже не вірили червоним білдам. Я почав з даних: витягнув з історії, які тести мигають, і їх виявилося всього дванадцять. Ми винесли їх у карантин з власниками, а щотижня на стендапі дивилися на цей список. За півтора місяця лишилось два, а головне — червоний гейт знову став означати баг.
🗣 How to say it out loud When I joined the project, developers pressed rerun about twice per PR and no longer trusted red builds. I started with data: I pulled from the history which tests flicker, and there were only twelve of them. We moved them into quarantine with owners and reviewed that list at the weekly standup. Within six weeks only two were left, and more importantly, a red gate meant a bug again.
99 Як налаштувати повторні запуски впалих тестів і не сховати ними справжні баги? How do you configure retries of failed tests without hiding real bugs behind them?
Flaky-тести, паралелізм і розбір падінь Flaky tests, parallelism and failure triage middle ▶

Що це — retry автоматично перезапускає тест після падіння. У Maven це -Dsurefire.rerunFailingTestsCount=2 (працює і з JUnit5), у JUnit Pioneer — анотація @RetryingTest(3), у TestNG — IRetryAnalyzer, у Playwright — retries у конфігу.

What it is — a retry automatically reruns a test after it fails. In Maven that is -Dsurefire.rerunFailingTestsCount=2 (works with JUnit5 too), in JUnit Pioneer the @RetryingTest(3) annotation, in TestNG an IRetryAnalyzer, in Playwright retries in the config.

Навіщо — у CI бувають збої, не пов'язані з кодом: мережа, холодний старт контейнера, тимчасово недоступний стенд. Один повтор не дає такому шуму заблокувати злиття. Важливо, що нормальні інструменти не приховують повтор: Surefire пише тест як flaky у звіті, Playwright показує статус «flaky», а не «passed».

Why — CI has failures unrelated to code: network, a container cold start, a temporarily unavailable environment. One retry stops that noise from blocking a merge. Importantly, good tools do not hide the retry: Surefire records the test as flaky in its report, and Playwright shows the status "flaky", not "passed".

Приклад — мої правила: retries лише в CI (process.env.CI ? 2 : 0), локально — нуль, щоб розробник бачив проблему одразу; не більше двох повторів; кожен flaky-результат потрапляє у звіт і метрику. У Playwright я вмикаю trace: 'on-first-retry', тож перший повтор одразу дає trace для розбору.

Example — my rules: retries only in CI (process.env.CI ? 2 : 0), zero locally so a developer sees the problem immediately; no more than two retries; every flaky result goes into the report and the metric. In Playwright I enable trace: 'on-first-retry', so the first retry immediately produces a trace to investigate.

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

Pitfalls — retries mask genuine races in the product: if a double click creates two orders one time in ten, a test with retries stays green while users do not. The second trap is retrying tests with side effects: the second attempt starts from the dirty data left by the first.

Уточнення на співбесіді — «Retry на весь набір чи на окремі тести?» Глобально в CI з лімітом 1–2, але з дашбордом flaky; анотацію на конкретний тест вважаю запахом.

Interview follow-up — "Retry the whole suite or individual tests?" Globally in CI with a limit of 1–2 but backed by a flaky dashboard; an annotation on a specific test I treat as a smell.

Maven, JUnit Pioneer і Playwright — повтори під контролем
Maven, JUnit Pioneer and Playwright — controlled retries
# Maven: до 2 повторів, у звіті тест позначиться як flaky
mvn -B test -Dsurefire.rerunFailingTestsCount=2

// JUnit Pioneer: точковий повтор (краще уникати)
@RetryingTest(maxAttempts = 3)
void uploadsLargeFile() { /* ... */ }

// playwright.config.ts
export default defineConfig({
  retries: process.env.CI ? 2 : 0,   // локально без повторів
  use: { trace: 'on-first-retry' }, // trace з першого повтору
  reporter: [['html'], ['junit', { outputFile: 'results.xml' }]],
});
# Maven: up to 2 reruns, the report marks the test as flaky
mvn -B test -Dsurefire.rerunFailingTestsCount=2

// JUnit Pioneer: a targeted retry (better avoided)
@RetryingTest(maxAttempts = 3)
void uploadsLargeFile() { /* ... */ }

// playwright.config.ts
export default defineConfig({
  retries: process.env.CI ? 2 : 0,   // no retries locally
  use: { trace: 'on-first-retry' }, // trace from the first retry
  reporter: [['html'], ['junit', { outputFile: 'results.xml' }]],
});
Що в коді: тернарний оператор вмикає повтори лише там, де задана змінна CI. on-first-retry записує trace тільки для тестів, що вже впали, — це дешево і дає матеріал для розбору.
What the code shows: the ternary enables retries only where the CI variable is set. on-first-retry records a trace only for tests that have already failed — cheap, and it gives you material to investigate.
💡 Простими словами: «зелений після повтору» — це не зелений, а жовтий. Дивись, щоб звіт і метрики показували його саме так.
💡 In plain words: "green after a retry" is not green, it's yellow. Make sure the report and the metrics show it that way.
🗣 Як читати вголос Я не проти retries, але ставлюся до них як до знеболювального. У нас у CI стоїть два повтори, локально нуль, і кожен тест, що пройшов лише з другої спроби, падає в окремий список flaky. Одного разу саме цей список показав тест оформлення замовлення, який мигав через справжню гонку на бекенді — без звіту про повтори ми б її ще довго не помітили.
🗣 How to say it out loud I'm not against retries, but I treat them like a painkiller. In our CI we have two retries, zero locally, and every test that only passed on the second attempt lands in a separate flaky list. Once that list surfaced a checkout test that flickered because of a real race on the backend — without the retry report we wouldn't have noticed it for a long time.
100 Як увімкнути паралельний запуск у JUnit5 і захистити спільні ресурси? How do you enable parallel execution in JUnit5 and protect shared resources?
Flaky-тести, паралелізм і розбір падінь Flaky tests, parallelism and failure triage senior ▶

Що це — JUnit Jupiter вміє виконувати тести в пулі потоків в одному JVM. Вмикається це у файлі src/test/resources/junit-platform.properties: прапорець parallel.enabled, режим за замовчуванням для методів і класів (same_thread або concurrent) і стратегія розміру пулу (dynamic від кількості ядер або fixed).

What it is — JUnit Jupiter can run tests in a thread pool within one JVM. You enable it in src/test/resources/junit-platform.properties: the parallel.enabled flag, the default mode for methods and classes (same_thread or concurrent) and the pool sizing strategy (dynamic based on cores, or fixed).

Коли і трейдоф — альтернатива — паралелізм процесами через Surefire forkCount: кожен форк — окремий JVM, статичні поля не перетинаються, але старт дорожчий і пам'яті треба більше. Потоки дешевші, зате вимагають thread-safe фреймворку: драйвер у ThreadLocal, жодних статичних змінних стану, унікальні тестові дані. Для тестів, які не можна розпаралелити, є @Execution(SAME_THREAD), а для доступу до спільного ресурсу (feature-флаг, системна властивість, один тестовий акаунт) — @ResourceLock: JUnit не запустить одночасно два тести з READ_WRITE-замком на той самий ключ.

When and trade-off — the alternative is process parallelism via Surefire forkCount: each fork is a separate JVM, static fields don't collide, but startup costs more and you need more memory. Threads are cheaper but require a thread-safe framework: the driver in a ThreadLocal, no static state, unique test data. For tests that cannot run in parallel there is @Execution(SAME_THREAD), and for access to a shared resource (a feature flag, a system property, a single test account) there is @ResourceLock: JUnit will not run two tests holding a READ_WRITE lock on the same key at the same time.

Приклад — для UI-набору я ставлю fixed з паралелізмом 4, бо кожен потік тримає браузер, а на CI-агенті 8 ГБ пам'яті; для API-тестів — dynamic.

Example — for a UI suite I use fixed with parallelism 4, since each thread holds a browser and the CI agent has 8 GB of memory; for API tests — dynamic.

Підводні камені — увімкнути паралельність у фреймворку, де драйвер статичний, і отримати тести, що клікають у чужих браузерах. Ще одна пастка — @BeforeAll зі спільним станом при concurrent для методів одного класу.

Pitfalls — enabling parallelism in a framework with a static driver and getting tests that click in each other's browsers. Another trap is @BeforeAll with shared state when methods of one class run concurrent.

Уточнення на співбесіді — «Як мігрувати поступово?» Спершу mode.classes.default=concurrent при методах у same_thread: класи йдуть паралельно, а всередині класу — послідовно.

Interview follow-up — "How do you migrate gradually?" Start with mode.classes.default=concurrent while methods stay same_thread: classes run in parallel, methods within a class run sequentially.

junit-platform.properties і анотації керування паралельністю
junit-platform.properties and the parallelism control annotations
# src/test/resources/junit-platform.properties
junit.jupiter.execution.parallel.enabled = true
# класи паралельно, методи всередині класу — послідовно
junit.jupiter.execution.parallel.mode.default = same_thread
junit.jupiter.execution.parallel.mode.classes.default = concurrent
junit.jupiter.execution.parallel.config.strategy = fixed
junit.jupiter.execution.parallel.config.fixed.parallelism = 4

// експорт пише в одну папку — лише послідовно
@Execution(ExecutionMode.SAME_THREAD)
class ExportTests { /* ... */ }

// тести, що перемикають спільний feature-флаг, не перетнуться
@Test
@ResourceLock(value = "feature-flags", mode = ResourceAccessMode.READ_WRITE)
void newCheckoutBehindFlag() { /* ... */ }
# src/test/resources/junit-platform.properties
junit.jupiter.execution.parallel.enabled = true
# classes in parallel, methods inside a class sequentially
junit.jupiter.execution.parallel.mode.default = same_thread
junit.jupiter.execution.parallel.mode.classes.default = concurrent
junit.jupiter.execution.parallel.config.strategy = fixed
junit.jupiter.execution.parallel.config.fixed.parallelism = 4

// export writes into one folder — sequential only
@Execution(ExecutionMode.SAME_THREAD)
class ExportTests { /* ... */ }

// tests toggling a shared feature flag will never overlap
@Test
@ResourceLock(value = "feature-flags", mode = ResourceAccessMode.READ_WRITE)
void newCheckoutBehindFlag() { /* ... */ }
Що в коді: комбінація mode.default = same_thread і mode.classes.default = concurrent — найбезпечніший старт. @ResourceLock з однаковим ключем серіалізує лише тести, що ділять ресурс, решта йде паралельно.
What the code shows: combining mode.default = same_thread with mode.classes.default = concurrent is the safest start. @ResourceLock with the same key serialises only the tests sharing that resource; the rest still run in parallel.
💡 Простими словами: паралельність вмикається одним рядком, а заробляється архітектурою. Спершу прибери статичний стан, потім збільшуй кількість потоків.
💡 In plain words: parallelism is switched on with one line but earned through architecture. Remove static state first, then raise the thread count.
🗣 Як читати вголос Коли ми вмикали паралельність, я не став одразу ставити concurrent для всього. Спершу паралельно пішли лише класи, і навіть це відкрило три тести, що ділили один акаунт. Їх ми або переписали на унікальних користувачів, або закрили ResourceLock. Потім я підібрав кількість потоків під пам'ять агента — з чотирма браузерами набір пройшов утричі швидше і без нових падінь.
🗣 How to say it out loud When we turned on parallelism, I didn't set concurrent for everything straight away. First only classes ran in parallel, and even that exposed three tests sharing one account. We either rewrote them to use unique users or guarded them with a ResourceLock. Then I tuned the thread count to the agent's memory — with four browsers the suite ran three times faster with no new failures.
101 Як розбити довгий набір тестів на шарди між CI-джобами і зібрати єдиний звіт? How do you shard a long test suite across CI jobs and merge a single report?
Flaky-тести, паралелізм і розбір падінь Flaky tests, parallelism and failure triage senior ▶

Що це — шардинг ділить набір на N частин, і кожна виконується на окремій машині (джобі) одночасно. Це наступний рівень після паралельних потоків: одна машина впирається в CPU і пам'ять, а шарди масштабуються горизонтально. Час прогону наближається до «загальний час / N плюс накладні витрати на старт».

What it is — sharding splits the suite into N parts, each running on a separate machine (job) at the same time. It's the next level after parallel threads: one machine hits its CPU and memory limits, while shards scale horizontally. Run time approaches "total time / N plus startup overhead".

Коли і трейдоф — має сенс, коли набір іде довше 15–20 хвилин і вже розпаралелений на одній машині. У Playwright вбудовано --shard=1/4 і blob-репортер: кожен шард вивантажує blob-report як артефакт, а окрема джоба запускає merge-reports. У Java немає стандартного прапорця, тому ділять за тегами, пакетами або за історією тривалості (щоб шарди були рівні за часом, а не за кількістю тестів); Allure-результати з усіх джоб просто зливають в одну папку перед allure generate. Трейдоф — більше хвилин CI і складніший конвеєр.

When and trade-off — it pays off when the suite takes over 15–20 minutes and is already parallelised on one machine. Playwright has built-in --shard=1/4 and a blob reporter: each shard uploads its blob-report as an artifact, and a separate job runs merge-reports. Java has no standard flag, so you split by tags, packages or by duration history (so shards are balanced by time, not test count); Allure results from all jobs are simply merged into one folder before allure generate. The trade-off is more CI minutes and a more complex pipeline.

Приклад — 40-хвилинний E2E-набір на чотирьох шардах пройшов за 12 хвилин разом зі встановленням браузерів.

Example — a 40-minute E2E suite on four shards finished in 12 minutes, browser installation included.

Підводні камені — fail-fast у матриці скасовує інші шарди після першого падіння, і звіт виходить неповним. Шарди також б'ють по одному стенду одночасно, тому тестові дані мають бути ізольовані.

Pitfalls — fail-fast in the matrix cancels the other shards after the first failure, leaving an incomplete report. Shards also hit the same environment simultaneously, so test data must be isolated.

Уточнення на співбесіді — «Скільки шардів?» Доти, доки накладні витрати на старт не з'їдають виграш; зазвичай 4–8.

Interview follow-up — "How many shards?" Until startup overhead eats the gain; usually 4–8.

GitHub Actions — Playwright на 4 шардах і злиття звітів
GitHub Actions — Playwright on 4 shards and report merging
jobs:
  e2e:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false          # не зупиняти інші шарди
      matrix: { shard: [1, 2, 3, 4] }
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npx playwright install --with-deps
      # у конфігу для CI: reporter 'blob'
      - run: npx playwright test --shard=${{ matrix.shard }}/4
      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with: { name: blob-${{ matrix.shard }}, path: blob-report }
  merge:
    needs: e2e
    if: ${{ !cancelled() }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - uses: actions/download-artifact@v4
        with: { pattern: blob-*, path: all-blobs, merge-multiple: true }
      # один HTML-звіт з усіх шардів
      - run: npx playwright merge-reports --reporter html ./all-blobs
jobs:
  e2e:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false          # don't stop the other shards
      matrix: { shard: [1, 2, 3, 4] }
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npx playwright install --with-deps
      # the CI config uses reporter 'blob'
      - run: npx playwright test --shard=${{ matrix.shard }}/4
      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with: { name: blob-${{ matrix.shard }}, path: blob-report }
  merge:
    needs: e2e
    if: ${{ !cancelled() }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - uses: actions/download-artifact@v4
        with: { pattern: blob-*, path: all-blobs, merge-multiple: true }
      # one HTML report from all shards
      - run: npx playwright merge-reports --reporter html ./all-blobs
Що в коді: матриця створює чотири паралельні джоби, кожна отримує свій номер шарда. if: !cancelled() гарантує, що артефакти й злиття виконаються навіть після падіння тестів.
What the code shows: the matrix creates four parallel jobs, each getting its own shard number. if: !cancelled() ensures artifacts and the merge still run after test failures.
💡 Простими словами: спершу вичави паралельність на одній машині, потім шардуй. І завжди збирай один звіт — чотири окремі ніхто не читатиме.
💡 In plain words: squeeze parallelism out of one machine first, then shard. And always merge into one report — nobody will read four separate ones.
🗣 Як читати вголос У нас E2E ішов сорок хвилин, і розробники просто перестали чекати на результат перед злиттям. Я розбив набір на чотири шарди через матрицю GitHub Actions і додав окрему джобу, яка зливає blob-звіти в один HTML. Вийшло дванадцять хвилин. Один урок я засвоїв на практиці: вимкнути fail-fast, бо без цього після першого червоного шарда інші скасовувалися і ми бачили лише чверть картини.
🗣 How to say it out loud Our E2E suite ran for forty minutes, and developers simply stopped waiting for results before merging. I split it into four shards with a GitHub Actions matrix and added a separate job that merges the blob reports into one HTML. It came down to twelve minutes. One lesson I learned the hard way: turn off fail-fast, because otherwise after the first red shard the others were cancelled and we only saw a quarter of the picture.
102 Як ви розбираєте червоний прогін у CI: баг продукту, баг тесту чи проблема середовища? How do you triage a red CI run: product bug, test bug or environment problem?
Flaky-тести, паралелізм і розбір падінь Flaky tests, parallelism and failure triage senior ▶

Що це — Тріаж падіння — це послідовне з'ясування, чому прогін став червоним і хто має це виправити. Кожне падіння я кладу в одну з чотирьох корзин: дефект продукту; дефект тесту (застарілий локатор, неправильне очікування, залежність від порядку); середовище чи інфраструктура (стенд лежить, закінчився диск на агенті, недоступний сторонній сервіс); тестові дані (хтось видалив акаунт, зіпсований seed).

What it is — Failure triage is the methodical work of finding out why a run went red and who should fix it. I put every failure into one of four buckets: a product defect; a test defect (stale locator, wrong wait, order dependency); environment or infrastructure (the stand is down, the agent ran out of disk, a third-party service is unreachable); test data (someone deleted the account, the seed is corrupted).

Навіщо — Без класифікації команда звикає до червоного кольору («та це знову стенд»), і справжній баг тихо йде в реліз. Корзини живлять метрики: якщо 60% падінь за місяць — інфраструктура, гроші треба вкладати в стенд, а не переписувати тести.

Why — Without classification the team gets used to red ("it's the stand again") and a real bug quietly ships. The buckets also feed metrics: if 60% of last month's failures were infrastructure, the investment belongs in the environment, not in rewriting tests.

Приклад — Нічна регресія: 14 червоних тестів. Спершу групую за повідомленням помилки: у 12 однаковий Connection refused до payment-service — це одна задача на середовище, а не 12 розслідувань. Два інші падають на сумі знижки. Беру останній зелений і перший червоний коміти, дивлюся git log між ними — змінили правило округлення. Запускаю тест локально на тому ж коміті, відтворюється стабільно — отже, дефект продукту з посиланням на прогін.

Example — Nightly regression: 14 red tests. First I group by error message: 12 share the same Connection refused to payment-service — one environment ticket, not 12 investigations. The other two fail on a discount amount. I take the last green and first red commits and read git log between them — the rounding rule changed. I run the test locally on that commit, it reproduces every time, so it is a product defect linked to the run.

Підводні камені — Перезапускати весь пайплайн, доки не позеленіє; розбирати тест за тестом замість групування; не мати артефактів (скріншот, trace, лог HTTP-запиту), через що тріаж перетворюється на здогадки.

Pitfalls — Re-running the whole pipeline until it turns green; going test by test instead of grouping; having no artefacts (screenshot, trace, HTTP log), which turns triage into guesswork.

Уточнення на співбесіді — «Хто відповідає за тріаж?» Я пропоную тижневу ротацію чергового і правило: нічний прогін розібрано до стендапу, кожне падіння має мітку корзини й власника.

Interview follow-up — "Who owns triage?" I suggest a weekly on-call rotation and a rule: the nightly run is triaged before stand-up, and every failure gets a bucket label and an owner.

Shell — перші кроки тріажу
Shell — first triage steps
# 1. Які тести впали і з якими помилками — групуємо однакові
gh run view 9123456 --log-failed | grep -E "FAILED|Exception" | sort | uniq -c

# 2. Що змінилось між останнім зеленим і першим червоним прогоном
git log --oneline a1b2c3d..e4f5a6b -- src/

# 3. Відтворюємо один тест локально на тому ж коміті
git checkout e4f5a6b
mvn -Dtest=CheckoutDiscountTest#appliesRounding test

# 4. Чи живий стенд узагалі
curl -s -o /dev/null -w "%{http_code}" https://staging.example.com/health
# 1. Which tests failed and with which errors — group identical ones
gh run view 9123456 --log-failed | grep -E "FAILED|Exception" | sort | uniq -c

# 2. What changed between the last green and the first red run
git log --oneline a1b2c3d..e4f5a6b -- src/

# 3. Reproduce one test locally on the same commit
git checkout e4f5a6b
mvn -Dtest=CheckoutDiscountTest#appliesRounding test

# 4. Is the stand alive at all
curl -s -o /dev/null -w "%{http_code}" https://staging.example.com/health
Що в коді: --log-failed показує лише логи впалих кроків, а uniq -c одразу видає, скільки тестів мають ту саму причину. Діапазон a1b2c3d..e4f5a6b звужує пошук до комітів між зеленим і червоним прогонами.
What the code shows: --log-failed prints only the logs of failed steps, and uniq -c tells you at once how many tests share a cause. The range a1b2c3d..e4f5a6b narrows the search to commits between the green and the red run.
💡 Простими словами: Спочатку групуй падіння за причиною, потім шукай зміну між зеленим і червоним прогоном, і лише потім відкривай код тесту. Десять однакових помилок — це одна проблема.
💡 In plain words: Group failures by cause first, then look for the change between the green and the red run, and only then open the test code. Ten identical errors are one problem.
🗣 Як читати вголос На останньому проєкті в нас був черговий по регресії, і я кілька разів ним був. Ранок починався з того, що я відкривав нічний прогін і не ліз у кожен тест окремо, а дивився, скільки падінь мають одне й те саме повідомлення. Зазвичай виявлялось, що половина — це впалий сервіс на стенді, і це один тікет для DevOps. Решту я відтворював локально на тому ж коміті. Якщо відтворювалось стабільно — заводив баг продукту, якщо ні — це вже була історія про flaky-тест, і він ішов у карантин з власником.
🗣 How to say it out loud On my last project we had a regression on-call, and I took that role several times. My morning started with the nightly run, and instead of digging into each test I checked how many failures shared the same message. Usually half of them turned out to be a dead service on the stand, which is one ticket for DevOps. The rest I reproduced locally on the same commit. If it reproduces every time, I file a product bug; if it doesn't, it becomes a flaky-test story and goes to quarantine with an owner.
103 Що має містити баг-репорт, створений за падінням автотесту? What should a bug report raised from a failed automated test contain?
Flaky-тести, паралелізм і розбір падінь Flaky tests, parallelism and failure triage junior ▶

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

What it is — When an automated test finds a product defect, the bug report is written so a developer can reproduce it without running our framework. The test is only how we detected it; the report describes system behaviour: user steps or API calls, expected and actual result.

Навіщо — Репорт на кшталт «впав тест CheckoutTest, дивіться CI» повертають із питаннями. Порівняно з ручним багом тут є козир — готові докази: скріншот, Playwright trace, лог запиту й відповіді, точний коміт і збірка.

Why — A report like "CheckoutTest failed, see CI" bounces back with questions. Compared with a manual bug there is an advantage: evidence already exists — screenshot, Playwright trace, request and response log, the exact commit and build.

Приклад — Обов'язкові поля: заголовок про симптом («Знижка 10% округлюється вниз до цілої гривні»), середовище, збірка й коміт, кроки, переписані з кроків тесту людською мовою, очікуване/фактичне, вкладення, посилання на прогін і на сам тест, частота (стабільно чи 2 з 10 запусків). Якщо дефект знайдено на API, додаю готову curl-команду.

Example — Required fields: a symptom title ("10% discount is rounded down to a whole unit"), environment, build and commit, steps rewritten in plain language from the test steps, expected/actual, attachments, links to the run and to the test itself, frequency (always or 2 out of 10 runs). If the defect was found through the API, I add a ready curl command.

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

Pitfalls — Pasting a stack trace instead of describing behaviour; putting the test name in the title; filing a bug for a failure that is really a test or stand problem; omitting frequency, so the developer closes it as "cannot reproduce".

Уточнення на співбесіді — «Що робите з тестом, поки баг відкритий?» Позначаю його посиланням на баг (@Issue в Allure) і, якщо баг не блокер, тимчасово вимикаю з посиланням на тікет, щоб він не маскував нові падіння.

Interview follow-up — "What do you do with the test while the bug is open?" I link it to the bug (@Issue in Allure) and, if it is not a blocker, disable it temporarily with the ticket reference so it doesn't hide new failures.

Шаблон баг-репорту з автотесту
Bug report template from an automated test
Заголовок:   Знижка 10% округлюється вниз до цілої гривні в кошику
Середовище:  staging, збірка 2.14.0-rc3, коміт e4f5a6b, Chrome 129
Частота:     стабільно, 10 з 10 запусків
Кроки:
  1. Додати в кошик товар за 199.90
  2. Застосувати промокод SAVE10
Очікувано:   сума 179.91
Фактично:    сума 179.00
Докази:      screenshot.png, trace.zip, лог POST /api/cart/promo
API-репро:   curl -X POST https://staging.example.com/api/cart/promo -d '{"code":"SAVE10"}'
Прогін:      https://ci.example.com/runs/9123456
Тест:        CheckoutDiscountTest#appliesRounding
Title:       10% discount is rounded down to a whole unit in the cart
Environment: staging, build 2.14.0-rc3, commit e4f5a6b, Chrome 129
Frequency:   always, 10 out of 10 runs
Steps:
  1. Add an item priced 199.90 to the cart
  2. Apply promo code SAVE10
Expected:    total 179.91
Actual:      total 179.00
Evidence:    screenshot.png, trace.zip, log of POST /api/cart/promo
API repro:   curl -X POST https://staging.example.com/api/cart/promo -d '{"code":"SAVE10"}'
Run:         https://ci.example.com/runs/9123456
Test:        CheckoutDiscountTest#appliesRounding
Що в коді: Кроки описують дії користувача, а не методи фреймворку; технічні деталі (коміт, прогін, назва тесту) винесені в окремі поля внизу, щоб не заважати читати суть.
What the code shows: The steps describe user actions, not framework methods; technical details (commit, run, test name) sit in separate fields at the bottom so they don't get in the way of the substance.
💡 Простими словами: Перевір себе: чи зможе розробник відтворити баг, не знаючи, що наш фреймворк існує? Якщо так — репорт готовий.
💡 In plain words: Check yourself: could a developer reproduce the bug without knowing our framework exists? If yes, the report is ready.
🗣 Як читати вголос Я пишу такий репорт так, ніби автотесту взагалі не було: кроки людською мовою, очікуване й фактичне. А далі користуюся тим, що автоматизація дає безкоштовно — прикріплюю trace чи скріншот, лог запиту і посилання на конкретний прогін з комітом. Обов'язково пишу частоту, бо «падає раз на десять» і «падає завжди» — це зовсім різні розмови з розробником. І спершу переконуюсь, що це не стенд і не сам тест.
🗣 How to say it out loud I write that kind of report as if the automated test never existed: plain-language steps, expected and actual. Then I use what automation gives me for free — I attach the trace or screenshot, the request log and a link to the exact run with its commit. I always state the frequency, because "fails one time in ten" and "fails every time" are completely different conversations with a developer. And before filing I make sure it isn't the stand or the test itself.
▶ 12 7
104 Як розкласти тести по стадіях CI-пайплайна: що запускати на PR, а що вночі? How do you split tests into CI pipeline stages: what runs on a PR and what runs nightly?
CI-пайплайни, quality gates та інструменти CI pipelines, quality gates and tooling middle ▶

Що це — Стадійність означає, що різні набори тестів запускаються за різними подіями й з різним бюджетом часу. Типова схема: на кожен PR — лінтер, unit, API-тести і короткий UI smoke (разом до 10–15 хвилин); після злиття в main — ширший набір; уночі — повна регресія на всіх браузерах; після деплою — smoke на реальному середовищі.

What it is — Staging means different test sets run on different events with different time budgets. A typical layout: every PR gets lint, unit, API tests and a short UI smoke (10–15 minutes in total); after merge to main a wider set runs; at night the full regression across all browsers; after deploy a smoke against the real environment.

Навіщо — Розробник чекатиме на PR-гейт хвилин десять, але не годину: якщо гейт повільний, його почнуть обходити. Водночас повна регресія мусить десь жити, і ніч — дешеве вікно. Принцип fail fast: дешеві й швидкі перевірки йдуть першими, дорогі стартують лише тоді, коли дешеві зелені.

Why — A developer will wait ten minutes for a PR gate, not an hour; a slow gate gets bypassed. The full regression still needs a home, and the night is a cheap window. The fail-fast principle: cheap, quick checks go first, expensive ones start only when the cheap ones are green.

Приклад — У GitHub Actions я розділяю набори JUnit5-тегами (@Tag("smoke"), @Tag("regression")) і обираю їх через -Dgroups у Surefire. Залежність needs: зупиняє UI smoke, якщо впали API-тести, а умова if: з github.event_name відрізняє PR від нічного запуску за schedule.

Example — In GitHub Actions I split the sets with JUnit5 tags (@Tag("smoke"), @Tag("regression")) and select them with -Dgroups in Surefire. The needs: dependency stops the UI smoke when API tests fail, and an if: on github.event_name tells a PR apart from the nightly schedule run.

Підводні камені — Нічна регресія, яку ніхто не читає; smoke, що розрісся до 300 тестів; відсутність стадії після деплою, через що зламану конфігурацію продакшену ловлять користувачі.

Pitfalls — A nightly regression nobody reads; a smoke that has grown to 300 tests; no post-deploy stage, so broken production configuration is caught by users.

Уточнення на співбесіді — «Як вирішуєте, що потрапляє в smoke?» Критичні бізнес-потоки (логін, пошук, оплата) і тести, що найчастіше ловили реальні регресії, з жорстким лімітом часу.

Interview follow-up — "How do you decide what goes into smoke?" Critical business flows (login, search, payment) plus the tests that caught real regressions most often, under a hard time limit.

GitHub Actions — стадії тестів
GitHub Actions — test stages
name: tests
on:
  pull_request:
  schedule:
    - cron: '0 2 * * *'            # повна регресія о 02:00 UTC
jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: '21', cache: maven }
      - run: mvn -B verify -Dgroups=unit    # лінтер + unit, ~3 хв
  api:
    needs: unit                           # не стартує, якщо unit червоний
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: '21', cache: maven }
      - run: mvn -B test -Dgroups=api
  ui:
    needs: api
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: '21', cache: maven }
      - if: github.event_name == 'pull_request'
        run: mvn -B test -Dgroups=smoke     # короткий smoke для PR
      - if: github.event_name == 'schedule'
        run: mvn -B test -Dgroups=regression # уночі — усе
name: tests
on:
  pull_request:
  schedule:
    - cron: '0 2 * * *'            # full regression at 02:00 UTC
jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: '21', cache: maven }
      - run: mvn -B verify -Dgroups=unit    # lint + unit, ~3 min
  api:
    needs: unit                           # does not start if unit is red
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: '21', cache: maven }
      - run: mvn -B test -Dgroups=api
  ui:
    needs: api
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: '21', cache: maven }
      - if: github.event_name == 'pull_request'
        run: mvn -B test -Dgroups=smoke     # short smoke for a PR
      - if: github.event_name == 'schedule'
        run: mvn -B test -Dgroups=regression # everything at night
Що в коді: needs будує ланцюжок unit → api → ui, тому дорогий UI-джоб не витрачає хвилини, коли вже впав дешевший. Surefire передає -Dgroups у JUnit Platform як фільтр тегів.
What the code shows: needs chains unit → api → ui, so the expensive UI job doesn't burn minutes once a cheaper one has failed. Surefire passes -Dgroups to the JUnit Platform as a tag filter.
💡 Простими словами: Дай PR-гейту бюджет часу і захищай його: кожен новий тест у smoke має когось витіснити або окупитися реально знайденими регресіями.
💡 In plain words: Give the PR gate a time budget and defend it: every new smoke test must push another one out or earn its place through regressions it actually caught.
🗣 Як читати вголос Я думаю про пайплайн як про воронку. На PR мені потрібна відповідь за десять хвилин, тому там лише unit, API і маленький smoke з критичних сценаріїв. Якщо щось дешевше вже червоне, дорогий UI навіть не стартує. Повну регресію на всіх браузерах ми ганяли вночі, і в мене було правило: зранку хтось обов'язково її розбирає, інакше вона швидко перетворюється на шум. І окремо — короткий smoke одразу після деплою.
🗣 How to say it out loud I think of the pipeline as a funnel. On a PR I want an answer in ten minutes, so it's only unit, API and a small smoke of critical flows. If something cheaper is already red, the expensive UI job never starts. We ran the full cross-browser regression at night, and my rule was that someone triages it every morning, otherwise it turns into noise very quickly. And separately there's a short smoke straight after each deploy.
105 Як запускати UI-тести в Docker у CI і чому браузер падає без збільшеного /dev/shm? How do you run UI tests in Docker on CI, and why does the browser crash without a bigger /dev/shm?
CI-пайплайни, quality gates та інструменти CI pipelines, quality gates and tooling middle ▶

Що це — UI-тести в CI запускають у контейнерах, щоб версії браузера, драйвера й системних бібліотек були однакові на ноутбуці й на агенті. Два поширені варіанти: для Selenium — контейнер selenium/standalone-chrome (або Grid) плюс контейнер з тестами, що підключається через RemoteWebDriver; для Playwright — офіційний образ mcr.microsoft.com/playwright, де вже є браузери потрібної версії.

What it is — UI tests on CI run in containers so the browser, driver and system library versions are identical on a laptop and on the agent. Two common setups: for Selenium, a selenium/standalone-chrome container (or a Grid) plus a test container that connects via RemoteWebDriver; for Playwright, the official mcr.microsoft.com/playwright image, which already ships browsers of the right version.

Навіщо — Chrome активно використовує спільну пам'ять /dev/shm, а Docker за замовчуванням дає лише 64 МБ. На важких сторінках вкладка падає з «session deleted because of page crash» — і це виглядає як flaky-тест. Тому Selenium-контейнеру дають shm_size: 2gb, а для Playwright документація радить --ipc=host.

Why — Chrome relies heavily on shared memory in /dev/shm, and Docker gives only 64 MB by default. On heavy pages the tab dies with "session deleted because of page crash", which looks exactly like a flaky test. That is why the Selenium container gets shm_size: 2gb, and the Playwright docs recommend --ipc=host.

Приклад — У docker compose піднімаю застосунок із healthcheck, Chrome-контейнер і контейнер з Maven. Тести стартують лише після того, як застосунок здоровий, отримують BASE_URL і адресу браузера через змінні середовища, а звіти потрапляють назовні через volume і потім вантажаться як артефакт CI.

Example — In docker compose I bring up the app with a healthcheck, a Chrome container and a Maven container. Tests start only after the app is healthy, get BASE_URL and the browser address from environment variables, and reports reach the host through a volume to be uploaded as a CI artefact.

Підводні камені — Тег latest замість зафіксованої версії; версія образу Playwright, що не збігається з версією @playwright/test у package.json; localhost у BASE_URL, хоча всередині мережі compose застосунок доступний за ім'ям сервісу.

Pitfalls — Using the latest tag instead of a pinned version; a Playwright image version that differs from @playwright/test in package.json; localhost in BASE_URL although inside the compose network the app is reachable by its service name.

Уточнення на співбесіді — «Headless чи headed у CI?» Headless: швидше й не потребує дисплея; для розбору падінь покладаюся на скріншоти, відео чи trace, а не на спостереження за вікном.

Interview follow-up — "Headless or headed on CI?" Headless: faster and needs no display; to debug failures I rely on screenshots, video or trace rather than watching a window.

docker-compose.yml для UI-тестів
docker-compose.yml for UI tests
services:
  app:
    image: registry.example.com/shop:2.14.0
    healthcheck:                       # в образі має бути curl
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 5s
      retries: 20
  chrome:
    image: selenium/standalone-chrome:4.25.0   # версію фіксуємо
    shm_size: 2gb                       # інакше вкладка падає на важких сторінках
  tests:
    image: maven:3.9-eclipse-temurin-21
    depends_on:
      app: { condition: service_healthy }
      chrome: { condition: service_started }
    environment:
      BASE_URL: http://app:8080          # ім'я сервісу, не localhost
      REMOTE_URL: http://chrome:4444
    working_dir: /work
    volumes: [".:/work"]                # target/ зі звітами лишається на хості
    command: mvn -B test -Dgroups=ui

# Playwright: образ тієї ж версії, що й @playwright/test
# docker run --rm --ipc=host -v "$PWD":/work -w /work \
#   mcr.microsoft.com/playwright:v1.48.0-noble npx playwright test
services:
  app:
    image: registry.example.com/shop:2.14.0
    healthcheck:                       # the image must contain curl
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 5s
      retries: 20
  chrome:
    image: selenium/standalone-chrome:4.25.0   # pin the version
    shm_size: 2gb                       # otherwise the tab crashes on heavy pages
  tests:
    image: maven:3.9-eclipse-temurin-21
    depends_on:
      app: { condition: service_healthy }
      chrome: { condition: service_started }
    environment:
      BASE_URL: http://app:8080          # service name, not localhost
      REMOTE_URL: http://chrome:4444
    working_dir: /work
    volumes: [".:/work"]                # target/ with reports stays on the host
    command: mvn -B test -Dgroups=ui

# Playwright: an image of the same version as @playwright/test
# docker run --rm --ipc=host -v "$PWD":/work -w /work \
#   mcr.microsoft.com/playwright:v1.48.0-noble npx playwright test
Що в коді: condition: service_healthy змушує тести чекати, поки healthcheck застосунку стане зеленим, а не просто поки контейнер запуститься. shm_size і --ipc=host вирішують одну й ту саму проблему нестачі спільної пам'яті.
What the code shows: condition: service_healthy makes the tests wait until the app healthcheck passes, not merely until the container starts. shm_size and --ipc=host solve the same shared-memory shortage.
💡 Простими словами: Якщо UI-тести падають лише в Docker з «page crash» чи «tab crashed», першим ділом перевір розмір /dev/shm, а не локатори.
💡 In plain words: If UI tests fail only in Docker with "page crash" or "tab crashed", check the /dev/shm size first, not the locators.
🗣 Як читати вголос У нас була класична історія: локально все зелене, а в CI випадково падають тести на сторінці з великою таблицею. Виявилось, що Chrome у контейнері мав стандартні 64 мегабайти спільної пам'яті. Після shm_size падіння зникли. Відтоді я завжди фіксую версії образів, у compose чекаю на healthcheck застосунку, а не на старт контейнера, і стежу, щоб образ Playwright збігався з версією пакета.
🗣 How to say it out loud We had the classic story: everything green locally, but on CI tests randomly died on a page with a huge table. It turned out Chrome in the container had the default 64 megabytes of shared memory. After setting shm_size the failures disappeared. Since then I always pin image versions, make compose wait for the app healthcheck rather than just the container start, and keep the Playwright image in line with the package version.
106 Як зсунути перевірки коду автотестів вліво: pre-commit хуки та обов'язкові перевірки PR? How do you shift checks on automation code left with pre-commit hooks and required PR checks?
CI-пайплайни, quality gates та інструменти CI pipelines, quality gates and tooling middle ▶

Що це — Shift-left для автоматизації — це перенесення перевірок якомога ближче до моменту написання коду: частина спрацьовує ще на комп'ютері автора (pre-commit хуки: форматування, лінтер, пошук секретів), частина — як обов'язкові перевірки PR, без яких GitHub чи GitLab не дасть злити гілку (збірка, тести, quality gate SonarQube на новому коді).

What it is — Shift-left for automation means moving checks as close as possible to the moment code is written: some run on the author's machine (pre-commit hooks: formatting, linter, secret scanning), others as required PR checks without which GitHub or GitLab won't allow a merge (build, tests, SonarQube quality gate on new code).

Навіщо — Чим раніше знайдено проблему, тим дешевше її виправити: зайвий пробіл, закомічений токен чи Thread.sleep краще зловити за дві секунди локально, ніж через двадцять хвилин у CI або на рев'ю. До того ж рев'юер перестає витрачати увагу на стиль і дивиться на логіку тесту.

Why — The earlier a problem is found, the cheaper the fix: a stray space, a committed token or a Thread.sleep is better caught in two seconds locally than twenty minutes later in CI or in review. Reviewers also stop spending attention on style and look at test logic instead.

Приклад — У Java-фреймворку тестів я ставлю фреймворк pre-commit з хуками detect-private-key, check-merge-conflict і локальним spotless:check. У TS-проєкті те саме роблять husky і lint-staged з ESLint, включно з правилом playwright/no-wait-for-timeout. У CI той самий набір повторюється (хуки можна пропустити через --no-verify), а branch protection робить ці джоби обов'язковими.

Example — In a Java test framework I install the pre-commit framework with the detect-private-key and check-merge-conflict hooks plus a local spotless:check. In a TS project husky and lint-staged with ESLint do the same, including the playwright/no-wait-for-timeout rule. CI repeats the same set (hooks can be skipped with --no-verify), and branch protection makes those jobs required.

Підводні камені — Важкі хуки, що ганяють увесь набір тестів на кожен коміт, — люди їх вимикають; quality gate на всьому коді замість нового, через що старий борг блокує кожен PR; хуки без дублювання в CI.

Pitfalls — Heavy hooks that run the whole suite on every commit, which people then disable; a quality gate on all code rather than new code, so old debt blocks every PR; hooks without the same checks in CI.

Уточнення на співбесіді — «А тести пишуться теж зліва?» Так: автотест на історію — частина Definition of Done, тож він приходить в тому ж PR, що й фіча, а не спринтом пізніше.

Interview follow-up — "Are tests shifted left too?" Yes: the automated test for a story is part of the Definition of Done, so it arrives in the same PR as the feature, not a sprint later.

.pre-commit-config.yaml і крок quality gate
.pre-commit-config.yaml and a quality gate step
# .pre-commit-config.yaml — запускається при кожному git commit
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: check-merge-conflict
      - id: detect-private-key          # ключ не потрапить у репозиторій
  - repo: local
    hooks:
      - id: spotless
        name: spotless check
        entry: mvn -q spotless:check
        language: system
        pass_filenames: false
        files: \.java$                   # лише коли змінено Java-файли

# Крок у CI: PR червоний, якщо quality gate на новому коді не пройдено
- run: mvn -B verify sonar:sonar -Dsonar.qualitygate.wait=true
# .pre-commit-config.yaml — runs on every git commit
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: check-merge-conflict
      - id: detect-private-key          # a key never reaches the repo
  - repo: local
    hooks:
      - id: spotless
        name: spotless check
        entry: mvn -q spotless:check
        language: system
        pass_filenames: false
        files: \.java$                   # only when Java files changed

# CI step: the PR is red if the quality gate on new code fails
- run: mvn -B verify sonar:sonar -Dsonar.qualitygate.wait=true
Що в коді: pass_filenames: false запускає Spotless один раз для проєкту, а не для кожного файлу окремо. sonar.qualitygate.wait=true змушує Maven дочекатися вердикту SonarQube і завершитися з помилкою, якщо гейт червоний.
What the code shows: pass_filenames: false runs Spotless once for the project rather than per file. sonar.qualitygate.wait=true makes Maven wait for the SonarQube verdict and fail if the gate is red.
💡 Простими словами: Локальний хук має тривати секунди; усе довше — у CI. І будь-яка локальна перевірка повинна дублюватися в пайплайні, бо хук завжди можна обійти.
💡 In plain words: A local hook should take seconds; anything longer belongs in CI. And every local check must be repeated in the pipeline, because a hook can always be bypassed.
🗣 Як читати вголос Для мене shift-left у тестовому репозиторії — це дуже практична річ. Колись у нас в історію git потрапив токен тестового акаунта, і після цього я додав pre-commit із пошуком приватних ключів і форматуванням. Хуки швидкі, секунди дві. А все важке — збірка, тести, SonarQube на новому коді — живе в обов'язкових перевірках PR. Так на рев'ю ми більше не сперечаємося про відступи, а говоримо про те, що саме тест перевіряє.
🗣 How to say it out loud For me shift-left in a test repository is very practical. We once had a test account token land in git history, and after that I added pre-commit with private key detection and formatting. The hooks are fast, a couple of seconds. Everything heavy — build, tests, SonarQube on new code — lives in required PR checks. So in review we no longer argue about indentation; we talk about what the test actually verifies.
107 На що ви дивитесь під час код-рев'ю автотестів? What do you look for when reviewing automated test code?
CI-пайплайни, quality gates та інструменти CI pipelines, quality gates and tooling middle ▶

Що це — Рев'ю тестового коду відрізняється від рев'ю продакшен-коду: головне питання не «чи працює», а «чи впаде цей тест тоді й лише тоді, коли зламається перевірювана поведінка». Мій чекліст: назва описує поведінку; одна причина падіння; немає Thread.sleep; немає зашитих URL, паролів і спільних акаунтів; локатори за домовленою політикою; змістовні асерти без if і try/catch, що ковтають помилки; тест прибирає за собою й може йти паралельно.

What it is — Reviewing test code differs from reviewing production code: the key question isn't "does it work" but "will this test fail when, and only when, the behaviour under test breaks". My checklist: the name describes behaviour; one reason to fail; no Thread.sleep; no hardcoded URLs, passwords or shared accounts; locators follow the agreed policy; meaningful assertions without an if or a try/catch that swallows errors; the test cleans up and can run in parallel.

Навіщо — Поганий тест гірший за відсутній: він або дає хибне відчуття безпеки (не може впасти), або стає flaky і привчає команду ігнорувати червоне. Рев'ю — найдешевше місце, щоб це зупинити.

Why — A bad test is worse than none: it either gives false confidence (it cannot fail) or turns flaky and teaches the team to ignore red. Review is the cheapest place to stop that.

Приклад — Найпідступніша знахідка, яку я бачу найчастіше, — тест без справжнього асерта: перевірка через if з System.out.println, що завжди зелена. Прошу автора свідомо зламати продукт (або підмінити очікуване значення) і показати, що тест червоніє. Другий частий пункт — логін під спільним admin, що ламає паралельний запуск.

Example — The sneakiest finding, and the one I see most, is a test with no real assertion: a check via if with System.out.println that is always green. I ask the author to break the product deliberately (or change the expected value) and show the test goes red. The second frequent item is logging in as a shared admin, which breaks parallel runs.

Підводні камені — Рев'ювити лише стиль, бо «це ж тести»; приймати PR, де тест жодного разу не запускали в CI; не дивитися, як зміна впливає на час прогону.

Pitfalls — Reviewing only style because "they're just tests"; approving a PR whose test has never run in CI; ignoring how the change affects suite duration.

Уточнення на співбесіді — «Хто рев'ює тести?» Ідеально — і QA, і розробник фічі: розробник бачить, чи тест перевіряє правильне правило, а QA — чи тест стабільний і вписується у фреймворк.

Interview follow-up — "Who reviews tests?" Ideally both a QA and the feature developer: the developer sees whether the right rule is checked, the QA whether the test is stable and fits the framework.

Java — тест до і після рев'ю
Java — a test before and after review
// До рев'ю
@Test
void test1() throws Exception {                    // назва нічого не каже
    driver.get("https://staging.shop.com/login");   // зашитий стенд
    driver.findElement(By.xpath("//div[3]/form/input[1]"))
          .sendKeys("admin");                        // крихкий XPath, спільний акаунт
    Thread.sleep(5000);                             // сліпе очікування
    if (driver.getPageSource().contains("Welcome")) {
        System.out.println("ok");                   // тест не може впасти
    }
}

// Після рев'ю
@Test
void activeUserSeesPersonalGreetingAfterLogin() {
    User user = users.createActive();               // власні дані, безпечно паралельно
    loginPage.open().loginAs(user);                  // BASE_URL із конфігу
    assertThat(dashboard.greeting())                  // явне очікування всередині методу
        .isEqualTo("Welcome, " + user.firstName());
}
// Before review
@Test
void test1() throws Exception {                    // the name says nothing
    driver.get("https://staging.shop.com/login");   // hardcoded stand
    driver.findElement(By.xpath("//div[3]/form/input[1]"))
          .sendKeys("admin");                        // brittle XPath, shared account
    Thread.sleep(5000);                             // blind wait
    if (driver.getPageSource().contains("Welcome")) {
        System.out.println("ok");                   // the test can never fail
    }
}

// After review
@Test
void activeUserSeesPersonalGreetingAfterLogin() {
    User user = users.createActive();               // own data, safe in parallel
    loginPage.open().loginAs(user);                  // BASE_URL comes from config
    assertThat(dashboard.greeting())                  // explicit wait inside the method
        .isEqualTo("Welcome, " + user.firstName());
}
Що в коді: Перша версія завжди зелена, бо if без else ніколи не кидає помилку. Друга має один AssertJ-асерт, власного користувача і жодної інфраструктурної деталі в тілі тесту.
What the code shows: The first version is always green because an if without an else never throws. The second has one AssertJ assertion, its own user and no infrastructure details in the test body.
💡 Простими словами: Найкорисніше питання на рев'ю тесту: «Покажи, як він червоніє». Тест, який ніхто не бачив червоним, ще нічого не довів.
💡 In plain words: The most useful review question for a test is "show me it going red". A test nobody has seen red has proved nothing yet.
🗣 Як читати вголос Коли я рев'юю тести, я насамперед питаю себе, чи може цей тест узагалі впасти. Одного разу я знайшов у нас десяток перевірок, які через if і println були зеленими за будь-якого стану продукту. Далі дивлюся на стабільність: сліпі паузи, спільний акаунт, крихкі XPath, зашиті адреси. І прошу автора показати червоний прогін — це займає хвилину, але відсіює більшість порожніх тестів.
🗣 How to say it out loud When I review tests, the first thing I ask myself is whether this test can fail at all. Once I found a dozen checks in our suite that were green no matter what the product did, because of an if with a println. Next I look at stability: blind pauses, a shared account, brittle XPaths, hardcoded addresses. And I ask the author to show me a red run — it takes a minute and filters out most hollow tests.
108 Як додати перевірку продуктивності в пайплайн, щоб вона справді могла зупинити реліз? How do you add a performance check to the pipeline so it can actually stop a release?
CI-пайплайни, quality gates та інструменти CI pipelines, quality gates and tooling middle ▶

Що це — Перевірка продуктивності в CI — це невеликий навантажувальний сценарій із порогами, які перетворюють метрики на «так/ні». У k6 це thresholds: якщо p95 часу відповіді більший за 500 мс або частка помилок вища за 1%, k6 run завершується з ненульовим кодом і джоб червоніє. У Gatling ту саму роль виконують assertions, у JMeter — запуск у non-GUI режимі з аналізом результатів окремим кроком.

What it is — A performance check in CI is a small load scenario with thresholds that turn metrics into a yes/no. In k6 these are thresholds: if p95 response time exceeds 500 ms or the error rate goes above 1%, k6 run exits with a non-zero code and the job turns red. In Gatling assertions play the same role; in JMeter you run in non-GUI mode and evaluate results in a separate step.

Навіщо — Графік у Grafana, на який ніхто не дивиться, не зупинить реліз, а червоний джоб — зупинить. Регресії продуктивності (N+1 запит, зникнений індекс) найдешевше ловити в тому ж PR чи нічному прогоні, що їх спричинив.

Why — A Grafana chart nobody looks at won't stop a release, a red job will. Performance regressions (an N+1 query, a dropped index) are cheapest to catch in the same PR or nightly run that introduced them.

Приклад — Я розділяю два рівні. Smoke-load на 20 віртуальних користувачів протягом двох хвилин проти виділеного perf-стенда — у нічному пайплайні або перед релізом, пороги базуються на виміряному baseline плюс запас. Повноцінні stress- і soak-тести йдуть окремо, за розкладом, на середовищі, наближеному до продакшену, і їх результат аналізує людина.

Example — I split two levels. A smoke load of 20 virtual users for two minutes against a dedicated perf stand runs nightly or before a release, with thresholds based on a measured baseline plus a margin. Full stress and soak tests run separately, on a schedule, in a production-like environment, and a person analyses the results.

Підводні камені — Ганяти навантаження на спільному staging, де паралельно працюють UI-тести й інші команди, — цифри шумні, а колеги отримують flaky; пороги «зі стелі» без baseline; тестувати лише один ендпоінт без think time.

Pitfalls — Running load on a shared staging where UI tests and other teams work at the same time — numbers are noisy and colleagues get flaky runs; thresholds pulled from thin air without a baseline; hammering a single endpoint with no think time.

Уточнення на співбесіді — «Чому p95, а не середнє?» Середнє ховає хвіст: 95% запитів по 100 мс і 5% по 5 секунд дають пристойне середнє, але кожен двадцятий користувач страждає.

Interview follow-up — "Why p95 and not the average?" The average hides the tail: 95% of requests at 100 ms and 5% at 5 seconds give a decent mean, yet one user in twenty suffers.

k6 — smoke-load із порогами
k6 — smoke load with thresholds
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  vus: 20,
  duration: '2m',
  thresholds: {
    http_req_duration: ['p(95)<500'], // 95% запитів швидші за 500 мс
    http_req_failed: ['rate<0.01'],   // менше 1% помилок
    checks: ['rate>0.99'],
  },
};

export default function () {
  const res = http.get(__ENV.BASE_URL + '/api/products?page=1');
  check(res, { 'статус 200': (r) => r.status === 200 });
  sleep(1);                               // think time між діями
}

// CI: k6 run -e BASE_URL=https://perf.example.com smoke-load.js
// порушений поріг → ненульовий код виходу → джоб червоний
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  vus: 20,
  duration: '2m',
  thresholds: {
    http_req_duration: ['p(95)<500'], // 95% of requests faster than 500 ms
    http_req_failed: ['rate<0.01'],   // under 1% errors
    checks: ['rate>0.99'],
  },
};

export default function () {
  const res = http.get(__ENV.BASE_URL + '/api/products?page=1');
  check(res, { 'status 200': (r) => r.status === 200 });
  sleep(1);                               // think time between actions
}

// CI: k6 run -e BASE_URL=https://perf.example.com smoke-load.js
// a breached threshold → non-zero exit code → red job
Що в коді: check лише рахує успішні перевірки й не зупиняє тест; вирішальними є thresholds, зокрема checks: rate>0.99, що перетворює перевірки на умову провалу. __ENV читає змінні, передані через -e.
What the code shows: check only counts passed checks and doesn't stop the test; thresholds decide, including checks: rate>0.99, which turns checks into a failure condition. __ENV reads variables passed with -e.
💡 Простими словами: Спершу зніми baseline на стабільному стенді, потім став поріг із запасом 20–30%. Поріг без baseline або вічно червоний, або ніколи.
💡 In plain words: Measure a baseline on a stable stand first, then set the threshold with a 20–30% margin. A threshold without a baseline is either always red or never.
🗣 Як читати вголос Я не намагаюся запхати повноцінне навантажувальне тестування в кожен PR. Натомість у нас був невеликий k6-сценарій на двадцять користувачів, який ганявся вночі проти окремого perf-стенда. Пороги на p95 і частку помилок ми виставили після тижня замірів. Одного разу він почервонів після міграції, де загубився індекс, і ми це побачили вранці, а не від користувачів. Великі stress-тести робили окремо й аналізували вручну.
🗣 How to say it out loud I don't try to cram full load testing into every PR. Instead we had a small k6 scenario with twenty users that ran nightly against a separate perf stand. We set thresholds on p95 and error rate after a week of measurements. Once it went red after a migration that lost an index, and we saw it in the morning rather than from users. Big stress tests we ran separately and analysed by hand.
109 Що ви пишете у стратегію автоматизації і як формулюєте критерії виходу, які реально працюють як гейт? What goes into your automation strategy, and how do you phrase exit criteria that really work as a gate?
CI-пайплайни, quality gates та інструменти CI pipelines, quality gates and tooling senior ▶

Що це — Стратегія автоматизації — коротка жива сторінка, а не 40-сторінковий документ. У ній: цілі (що хочемо отримати: швидший реліз, менше ручної регресії); обсяг по шарах (що покриваємо unit, API, UI, а що свідомо лишаємо ручним); інструменти й мова; середовища й тестові дані; інтеграція в CI (які набори на яких подіях); звітність; власники; метрики. Критерії виходу — перевірювані умови, за яких збірка може йти далі.

What it is — An automation strategy is a short living page, not a 40-page document. It covers goals (what we want: faster releases, less manual regression); scope per layer (what unit, API and UI cover, and what we deliberately leave manual); tools and language; environments and test data; CI integration (which sets run on which events); reporting; owners; metrics. Exit criteria are verifiable conditions under which a build may move on.

Навіщо — Без стратегії автоматизація росте хаотично: UI-тести дублюють API, ніхто не знає, хто лагодить падіння. Без вимірюваних критеріїв виходу реліз вирішують відчуттями: «ну, наче все зелене».

Why — Without a strategy automation grows chaotically: UI tests duplicate API ones and nobody knows who fixes failures. Without measurable exit criteria a release is decided by gut feeling: "well, it looks green".

Приклад — Робочі критерії звучать так: 100% smoke пройдено на release candidate; 0 відкритих blocker/critical; усі тести критичних потоків (оплата, реєстрація) зелені без ретраїв; частка flaky за 7 днів нижче 2%; кожен новий story має автотест або погоджений виняток. Кожен пункт автоматично перевіряється або з CI, або з Jira-фільтра, і гейт стоїть перед деплоєм на продакшен.

Example — Workable criteria read like this: 100% of smoke passed on the release candidate; 0 open blocker/critical bugs; all critical-flow tests (payment, sign-up) green without retries; 7-day flaky rate below 2%; every new story has an automated test or an agreed exception. Each item is checked automatically from CI or a Jira filter, and the gate sits before the production deploy.

Підводні камені — «95% тестів пройдено» без розрізнення, які саме впали (5% можуть бути оплатою); критерії, яких ніхто не дотримується, бо дедлайн важливіший; стратегія, написана один раз і забута.

Pitfalls — "95% of tests passed" without distinguishing which failed (the 5% may be payment); criteria nobody honours because the deadline wins; a strategy written once and forgotten.

Уточнення на співбесіді — «А якщо бізнес хоче релізити попри невиконаний критерій?» Це легітимне рішення, але його фіксують явно: хто прийняв ризик, які обхідні заходи, до якої дати виправлення.

Interview follow-up — "What if the business wants to release despite an unmet criterion?" That's a legitimate decision, but it is recorded explicitly: who accepted the risk, what mitigations exist, and the fix deadline.

Фрагмент стратегії автоматизації (YAML)
Automation strategy excerpt (YAML)
goal: "ручна регресія з 3 днів до 4 годин за 2 квартали"
layers:
  api:    бізнес-правила, валідація, права доступу   # основна маса перевірок
  ui:     8 критичних потоків end-to-end
  manual: вичитка текстів, UX нових екранів         # свідомо не автоматизуємо
stack: Java 21, JUnit5, RestAssured, Playwright for Java, Allure
ownership: тест пише автор фічі, падіння розбирає черговий QA
exit_criteria:                                   # гейт перед продакшеном
  - smoke на RC: 100% passed
  - відкриті blocker/critical: 0
  - критичні потоки зелені без ретраїв
  - flaky rate за 7 днів: < 2%
metrics: [тривалість PR-гейту, flaky rate, баги з продакшену за реліз]
goal: "manual regression from 3 days to 4 hours within 2 quarters"
layers:
  api:    business rules, validation, permissions   # the bulk of checks
  ui:     8 critical end-to-end flows
  manual: copy proofreading, UX of new screens      # deliberately not automated
stack: Java 21, JUnit5, RestAssured, Playwright for Java, Allure
ownership: the feature author writes the test, the on-call QA triages failures
exit_criteria:                                   # gate before production
  - smoke on RC: 100% passed
  - open blocker/critical: 0
  - critical flows green without retries
  - 7-day flaky rate: < 2%
metrics: [PR gate duration, flaky rate, production bugs per release]
Що в коді: Кожен критерій у exit_criteria — число чи булева умова, яку можна зчитати з CI або баг-трекера. Розділ manual так само важливий, як і автоматизований обсяг: він фіксує свідомі рішення.
What the code shows: Every item under exit_criteria is a number or a boolean condition that can be read from CI or the bug tracker. The manual section matters as much as the automated scope: it records deliberate decisions.
💡 Простими словами: Якщо критерій виходу не можна перевірити запитом до CI чи Jira, це побажання, а не критерій. Перепиши його в число.
💡 In plain words: If an exit criterion can't be checked with a query to CI or Jira, it's a wish, not a criterion. Rewrite it as a number.
🗣 Як читати вголос Коли я приходив на проєкт, де автоматизація «якось росла сама», я починав зі сторінки стратегії на одну-дві сторінки. Там чітко записано, що перевіряємо на API, що на UI, а що свідомо лишаємо людям. А критерії виходу я формулюю так, щоб їх міг перевірити скрипт: нуль критичних багів, smoke стовідсотково, критичні потоки без ретраїв. І якщо бізнес вирішує релізити попри це — нормально, але ризик хтось бере на себе письмово.
🗣 How to say it out loud When I joined a project where automation had "just grown by itself", I started with a strategy page of one or two pages. It states plainly what we check through the API, what through the UI, and what we deliberately leave to people. I phrase exit criteria so that a script could verify them: zero critical bugs, smoke at a hundred percent, critical flows without retries. And if the business decides to release anyway, that's fine, but someone takes on the risk in writing.
110 Як ви обираєте інструменти для нового проєкту автоматизації? How do you choose the tools for a new automation project?
CI-пайплайни, quality gates та інструменти CI pipelines, quality gates and tooling senior ▶

Що це — Вибір інструментів — інженерне рішення з наслідками на роки, тому я роблю його за критеріями, а не за модою. Основні: мова й стек команди (хто писатиме й підтримуватиме тести); тип застосунку (web, mobile, API, desktop); потрібні браузери й платформи; інтеграція з CI і звітністю; стабільність і швидкість; спільнота й документація; ліцензія та вартість; ризики вендор-локу.

What it is — Choosing tools is an engineering decision with consequences for years, so I make it by criteria, not by fashion. The main ones: the team's language and stack (who will write and maintain the tests); application type (web, mobile, API, desktop); required browsers and platforms; CI and reporting integration; stability and speed; community and documentation; licence and cost; vendor lock-in risk.

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

Why — Migrating a thousand tests between frameworks is very expensive. A tool the developers don't know condemns automation to living in isolation: nobody but QA will fix a test or add one.

Приклад — Команда фронтенду пише на TypeScript, бекенд на Kotlin, потрібні Chrome і Safari. Кандидати: Playwright TS і Selenium Java. Я роблю PoC тиждень: 3–5 реальних сценаріїв (логін, оплата з iframe, завантаження файлу), запуск у CI, звіт. Зважена матриця показала перевагу Playwright TS: та сама мова, що й у фронтенду, WebKit із коробки, trace для розбору падінь. API-тести лишили на JVM-стеку поруч із бекендом.

Example — The frontend team writes TypeScript, the backend Kotlin, and we need Chrome and Safari. Candidates: Playwright TS and Selenium Java. I run a one-week PoC: 3–5 real scenarios (login, payment inside an iframe, file upload), a CI run, a report. A weighted matrix favoured Playwright TS: the same language as the frontend, WebKit out of the box, trace for debugging failures. API tests stayed on the JVM stack next to the backend.

Підводні камені — Обирати за «hello world» на демо-сайті замість складних місць свого застосунку; вирішувати наодинці без розробників; недооцінювати вартість навчання команди й міграції наявних тестів.

Pitfalls — Choosing based on a "hello world" on a demo site instead of the hard parts of your own app; deciding alone without developers; underestimating the cost of training the team and migrating existing tests.

Уточнення на співбесіді — «А якщо вже є 800 тестів на Selenium?» Тоді переписування має окупитися; часто розумніше писати нові тести на новому інструменті, а старі переносити лише тоді, коли їх все одно треба чіпати.

Interview follow-up — "What if there are already 800 Selenium tests?" Then a rewrite must pay for itself; it is often wiser to write new tests with the new tool and migrate old ones only when they have to be touched anyway.

Зважена матриця вибору після PoC
Weighted selection matrix after a PoC
# оцінка 1–5, вага — наскільки критерій важливий для цього проєкту
критерій                        вага  Playwright TS  Selenium Java
мова команди (фронт на TS)       3        5              3
Safari / WebKit                  2        4              3
стабільність PoC у CI (10 прог.) 3        5              4
розбір падінь (trace, відео)     2        5              3
наявні напрацювання              1        2              4
зважений підсумок                         50             37

# рішення: UI — Playwright TS; API — лишаємо на JVM поруч із бекендом
# score 1–5, weight — how much the criterion matters for this project
criterion                       weight Playwright TS  Selenium Java
team language (frontend in TS)   3        5              3
Safari / WebKit                  2        4              3
PoC stability in CI (10 runs)    3        5              4
failure debugging (trace, video) 2        5              3
existing assets                  1        2              4
weighted total                            50             37

# decision: UI — Playwright TS; API stays on the JVM next to the backend
Що в коді: Підсумок — сума «вага × оцінка»: для Playwright 15 + 8 + 15 + 10 + 2 = 50, для Selenium 9 + 6 + 12 + 6 + 4 = 37. Ваги задаються до PoC, щоб результати не підганяли під улюбленця.
What the code shows: The total is the sum of weight × score: 15 + 8 + 15 + 10 + 2 = 50 for Playwright and 9 + 6 + 12 + 6 + 4 = 37 for Selenium. The weights are fixed before the PoC so results aren't bent towards a favourite.
💡 Простими словами: PoC роби на найскладніших місцях свого застосунку, а не на демо-сайті, і запускай його в CI десятки разів. Стабільність у пайплайні важить більше, ніж зручність API.
💡 In plain words: Build the PoC on the hardest parts of your own app, not a demo site, and run it in CI dozens of times. Stability in the pipeline matters more than a pleasant API.
🗣 Як читати вголос Я уникаю відповіді «найкращий інструмент — Playwright» без контексту. Спочатку питаю, хто писатиме тести і на якій мові працюють розробники, які браузери потрібні, що вже є в CI. Потім беру два кандидати й тиждень роблю PoC на найболючіших сценаріях нашого застосунку — з iframe, завантаженням файлів, ганяю їх у пайплайні десятки разів. Ваги критеріїв ми погоджуємо з командою заздалегідь, і рішення потім легко пояснити будь-кому.
🗣 How to say it out loud I avoid answering "the best tool is Playwright" without context. First I ask who will write the tests, what language the developers use, which browsers we need and what already exists in CI. Then I take two candidates and spend a week on a PoC with the most painful scenarios of our app — iframes, file uploads — running them in the pipeline dozens of times. We agree the criteria weights with the team up front, so the decision is easy to explain to anyone later.