⚔️ Битва підходів: Підготовка стану (UI-логін проти API-ін'єкції)Сьогоднішня битва присвячена найчастішій дії в автотестах — авторизації. Уявіть, що у вас 100 тест-кейсів, і для кожного юзер має бути залогінений. Як ми це робимо? ☕️🥊 Підхід 1: UI-авторизація (Як роблять новачки)У блоці beforeEach тест відкриває сторінку /login, знаходить поле email, вводить текст, знаходить password, вводить пароль, натискає кнопку і чекає на редирект.Плюси: Тест на 100% імітує дії реального користувача.Мінуси: Це катастрофічно повільно. Якщо UI-логін займає 3 секунди, на 100 тестах ви втрачаєте 5 хвилин просто на введення паролів. А якщо фронтендери змінять дизайн або додадуть капчу — впадуть усі 100 тестів одночасно.
🥊 Підхід 2: API-ін'єкція стану (Рівень Архітектора)Ви взагалі не відкриваєте сторінку логіну. Ви робите один швидкий HTTP-запит POST /api/auth, отримуєте JWT-токен або Cookie, і напряму "впорскуєте" його в контекст браузера.Плюси: Авторизація займає 50 мілісекунд замість 3 секунд. Тести повністю ізольовані від змін в UI екрану логіну.Мінуси: Виникає страх "А раптом сама форма логіну зламалася, а ми це пропустили?".
⚖️ Вердикт QA Co-pilot:
Запам'ятайте золоте правило автоматизації: Тестуйте UI логіну тільки в тестах на логін.Вам потрібен рівно ОДИН тест, який чесно проходить через UI форму авторизації (перевіряє валідацію, повідомлення про помилки тощо).Для всіх інших 99 тестів (кошик, профіль, пошук) екран логіну — це просто перешкода. Використовуйте API-ін'єкцію (у Playwright це робиться через request.post та browserContext.addCookies()), щоб миттєво підготувати стан і тестувати саме те, що треба.
💩 Код з душком: Спільні тестові дані (або Прокляття [email protected])Привіт, екіпаж! П'ятниця — час вивітрювати антипатерни. Сьогодні говоримо про звичку, яка працює на локальному комп'ютері, але перетворює CI/CD пайплайн на суцільне мінне поле.
☕️Знайдіть проблему в цьому підході://
❌ Антипатерн "Гуртожиток" (Shared State)test.beforeEach(async ({ page }) => { // Усі тести у сьюті логіняться під ОДНИМ юзером await loginPage.login('
[email protected]', 'Qwerty1234');});test('Користувач може видалити товар з кошика', async ({ page }) => { await cartPage.deleteItem(); await expect(page.locator('.empty-cart')).toBeVisible();});
Чому цей код тхне:Коли ви запускаєте ці тести по одному на своєму ноуті — все ідеально. Але щойно ви налаштовуєте паралельний запуск (наприклад, workers: 4 у
Playwright), починається хаос.Один тест намагається покласти товар у кошик, а паралельний тест ТІЄЇ Ж
СЕКУНДИ натискає кнопку "Очистити кошик" (бо вони сидять під одним акаунтом!). Тести починають вбивати один одного. З'являються сотні "плаваючих" (flaky) багів.
✅ Як це виглядає після код-рев'ю Senior-інженера:Тести мають дотримуватись принципу F.I.R.S.T. (зокрема, бути Isolated/Ізольованими). Кожен тест повинен жити у своєму вакуумі і генерувати власні унікальні дані.//
🚀 Ідеально чистий код (Data Isolation)test.beforeEach(async ({ page, api }) => { // Динамічно створюємо УНІКАЛЬНОГО юзера для КОЖНОГО тесту через API const testUser = await api.createUser(); await loginPage.login(testUser.email, testUser.password);});test('Користувач може видалити товар з кошика', async ({ page }) => { // Тест працює у повній ізоляції. Ніхто інший не чіпає цей кошик. await cartPage.deleteItem(); await expect(page.locator('.empty-cart')).toBeVisible();});
Золоте правило: Ніколи не хардкодьте імейли чи ID сутностей у тестах. Використовуйте патерн Data
Factory /
API-генерацію. Тест має сам створити собі "пісочницю", погратися в ній і (бажано) прибрати за собою.А ваші тести б'ються за один імейл?
👇🔥 — Ні, у нас API генерує унікальні дані для кожного прогону!
👀 — Грішу... У мене є файл test-users.json на 5 акаунтів...
🤯 — То ось чому мої паралельні тести вічно падають!
💩 Код з душком: Тести-Доміно (або Гріх спільного стану)Привіт, екіпаж! П'ятниця — час вивітрювати "смердючий" код. Сьогодні препаруємо архітектурний антипатерн, який на початку здається геніальною оптимізацією, а через півроку перетворює ваш CI/CD на повільне пекло. Говоримо про залежність тестів (Test Interdependence). ☕️Знайдіть проблему в цьому коді:// ❌ Як пишуть "оптимізатори" (Антипатерн "Доміно")let orderId: string;test.describe('Флоу замовлення', () => { test('Крок 1: Створити замовлення', async ({ page }) => { orderId = await createOrderThroughUI(page); expect(orderId).toBeDefined(); }); test('Крок 2: Оплатити замовлення', async ({ page }) => { // Використовує ID з попереднього тесту! await payForOrder(page, orderId); expect(await getOrderStatus()).toBe('Paid'); }); test('Крок 3: Видалити замовлення', async ({ page }) => { await deleteOrderThroughUI(page, orderId); });});
Чому цей код тхне:Ви порушили головне правило автоматизації — F.I.R.S.T. (Isolated). Ваші тести тепер склеєні суперклеєм.
1️⃣ Крихкість ланцюга: Якщо "Крок 1" падає через випадковий мікро-лаг мережі або рендеру, "Крок 2" і "Крок 3" автоматично стають червоними, бо змінна orderId порожня. У звіті у вас три зламаних тести, хоча реальна проблема лише в одному місці.2️⃣ Паралельність мертва: Ви не зможете запустити ці тести паралельно на кількох воркерах у Playwright (fullyParallel: true), бо вони вимагають суворої послідовності. Ваш пайплайн ніколи не буде швидким.3️⃣ Неможливість локального дебагу: Ви не можете просто взяти і запустити "Крок 3" ізольовано, щоб перевірити логіку видалення. Вам доведеться щоразу чекати, поки проклікаються попередні кроки.
✅ Як це виглядає після код-рев'ю Senior-інженера:Кожен тест має бути повністю незалежним. Він сам готує для себе ідеальні умови (бажано за мілісекунди через API) і сам за собою прибирає.// 🚀 Ідеально чистий код (Повна ізоляція)test('Повинен успішно оплатити замовлення', async ({ request, page }) => { // 1. Arrange: Блискавично створюємо замовлення "під капотом" (через бекенд) const order = await request.post('/api/orders').then(r => r.json()); // 2. Act: Тестуємо саме UI оплати await page.goto(`/orders/${order.id}/pay`); await page.getByRole('button', { name: 'Оплатити' }).click(); // 3. Assert: Перевіряємо результат await expect(page.locator('.status')).toHaveText('Paid');});
Золоте правило: Тест не повинен знати про існування інших тестів. Використовуйте API-виклики (request) або Fixtures для підготовки даних перед дією page.goto(). Кожен запуск — це чиста сторінка.А як у вас справи із залежністю тестів? 👇🔥 — Повна ізоляція, кожен тест готує дані через API за мілісекунди!👀 — У мене величезний test.describe.serial, інакше все ламається...🤯 — А що, тести можна запускати в довільному порядку?!
🔥 Прожарка інструментів: Cucumber (або Чому бізнес ніколи не читатиме ваші тести)Привіт, екіпаж! Четвер — час розпалювати гриль. Сьогодні на нашій решітці лежить інструмент, який продають менеджерам як "срібну кулю", а для інженерів він часто перетворюється на архітектурні кайдани. Зустрічайте — Cucumber та його синтаксис Gherkin. ☕️🟢 Як нам це продавали (Очікування):"Давайте писати тести людською мовою! Given, When, Then. Бізнес-аналітики та продакт-менеджери будуть самі писати сценарії, а автоматизатори — лише підкладати під них код. У нас буде ідеальна жива документація, яку зрозуміє навіть CEO!"
🥩 Прожарка (Сувора реальність):
Бізнес ніколи не пише тести. Це найголовніший міф індустрії. У 99% випадків аналітику немає коли гратися зі специфічним синтаксисом і відступами. У результаті QA-інженер сам вигадує сценарій на Gherkin, а потім сам же пише під нього код. Ви просто робите подвійну роботу.Прокляття текстових рядків: Замість гнучкого та типізованого TypeScript ви будуєте крихкий міст. Змінили формулювання кроку з "я натискаю кнопку Зберегти" на "я клікаю на кнопку Зберегти" — і тест відвалився, бо не спрацював регулярний вираз (Regex) під капотом.Біль рефакторингу та дебагу: Уявіть, що змінилася бізнес-логіка і треба переписати 50 тестів. У чистому Playwright з Page Object ви міняєте один метод, і IDE миттєво підсвічує всі місця, де він використовується. З Cucumber ви шукаєте зламані кроки по всьому проєкту через "Find in Files", створюючи жахливе нагромадження дублюючих Step Definitions.
⚖️ Вердикт QA Co-pilot:
BDD (Behavior-Driven Development) — це чудовий процес для обговорення вимог між розробником, тестувальником і бізнесом на етапі планування. Але тягнути BDD у код автотестів — це жорсткий оверінжиніринг. Сучасний Playwright з гарно названими кроками test.step('Заповнення форми оплати', ...) та згенерованим HTML-звітом дає максимально зрозумілу картину для будь-якого менеджера. Без жодних прошарків із "огірків".
А у вас в проєкті ростуть огірки? 👇🔥 — Давно викинули BDD, пишемо чистий код на TS і кайфуємо!👀 — Пишу на Gherkin, бо так сказав замовник. Плачу і підтримую регулярки...🤬 — Не згоден, Cucumber дисциплінує і робить тести читабельними для всіх!
🗂 Ультимативна шпаргалка: Жорсткі vs. М'які перевірки (Soft Assertions)Коротка шпаргалка про те, як зібрати максимум багів за один прогін і не переривати скрипт на півдорозі.
Зберігайте! ☕️🛑 expect() (Жорстка перевірка — Класика)
Як працює: Знайшов помилку
➡️ Тест миттєво впав (Fatal Error)
➡️ Усі наступні кроки скасовано.
Коли юзати: Критичні бізнес-кроки. Якщо після логіну юзер не потрапив у дашборд, немає сенсу намагатися клікати далі.
// Якщо сторінка не завантажилась, тест падає тут...await expect(page).toHaveURL('/dashboard'); // ...цей клік ніколи не виконаєтьсяawait page.getByRole('button', { name: 'Створити' }).click();
☁️ expect.soft() (М'яка перевірка — Рятівник часу)
Як працює: Знайшов помилку
➡️ Записав у лог (червоним)
➡️ Пішов виконувати тест далі! Скрипт впаде лише в самому кінці, але збере всі помилки.
Коли юзати: Перевірка великих форм, таблиць, візуальних атрибутів, де один баг (наприклад, неправильний колір тексту) не блокує перевірку інших полів.
// Якщо ім'я не збігається, тест запише помилку, але ВСЕ ОДНО перевірить вік та email!await expect.soft(page.locator('#name')).toHaveValue('Ivan');await expect.soft(page.locator('#age')).toHaveValue('25');await expect.soft(page.locator('#email')).toHaveValue('
[email protected]');
Золоте правило:Використовуйте expect() для навігації та перевірок станів, від яких залежить наступний крок. Використовуйте expect.soft(), коли тестуєте "пачку" незалежних даних (наприклад, перевіряєте всі дані у профілі користувача після збереження).
А ви використовуєте "м'які" перевірки? 👇🔥 — Так, expect.soft() ідеально підходить для перевірки таблиць!
👀 — Пишу звичайний expect, хай падає одразу, мені так спокійніше.
🤯 — Почекайте, Playwright вміє не зупиняти тест при помилці?!
⚔️ Битва підходів: Junior vs. QA Architect (Цикли проти Нативних ассертів)Сьогодні розберемо класичну ситуацію: вам треба перевірити масив елементів на сторінці (наприклад, список повідомлень у чаті або таблицю). Як витягнути та перевірити дані, не змусивши ваш тест "гальмувати"? ☕️❌ Підхід Junior-автоматизатораconst texts = [];const messages = page.locator('.chat-message');const count = await messages.count();for (let i = 0; i < count; i++) { // Повільно витягуємо текст по одному... texts.push(await messages.nth(i).textContent());}expect(texts).toContain('Замовлення успішне');
Чому це антипатерн: Це повільно і крихко. По-перше, count() стріляє миттєво і не чекає на рендер (часто повертає 0). По-друге, кожна ітерація циклу for робить окремий асинхронний запит до браузера. Якщо у чаті 50 повідомлень — тест гарантовано "зависне" на кілька секунд.✅ Підхід QA Architect// Варіант 1: Одразу перевіряємо наявність тексту у списку (з auto-retry)await expect(page.locator('.chat-message')).toContainText(['Замовлення успішне']);// Варіант 2: Якщо масив реально потрібен для хитрої логікиconst allTexts = await page.locator('.chat-message').allTextContents();
Чому це шедевр: Швидкість світла і стабільність. Playwright виконує allTextContents() або toContainText() за одну мілісекунду на рівні браузерного рушія, перехоплюючи весь масив одразу. Крім того, веб-ассерт автоматично зачекає (до 5 секунд), поки потрібне повідомлення не з'явиться в DOM, що рятує від Flaky-тестів.Золоте правило: Якщо ви пишете цикл for для перебору UI-елементів у Playwright — зупиніться. З імовірністю 99% ви робите щось не так і для цього вже є нативний оптимізований метод.А як ви працюєте зі списками елементів? 👇
💩 Код з душком: If/Else у тестах (або Тест із роздвоєнням особистості)Привіт, екіпаж! П'ятниця — традиційний час вивітрювати "смердючий" код з репозиторіїв. Сьогодні препаруємо гріх, який перетворює ваші автотести на непередбачуваний хаос. Поговоримо про умовну логіку (Conditional Testing). ☕️Знайдіть проблему в цьому тесті:// ❌ Як пишуть джуни (Антипатерн "Ворожка")test('Повинен додати товар у кошик', async ({ page }) => { await page.goto('/product/123'); // "Якщо раптом вилізе промо-банер, то закриємо його..." if (await page.locator('.promo-popup').isVisible()) { await page.locator('.close-promo').click(); } await page.locator('.add-to-cart').click();});
Чому цей код тхне:Ви щойно вбили детермінованість тесту (його передбачуваність). Метод isVisible() у Playwright не чекає! Він стріляє миттєво.Якщо ваш фронтенд на Angular рендерить цей поп-ап за 300 мілісекунд, на момент перевірки isVisible() поверне false. Тест проігнорує блок if і піде клікати на кнопку кошика. АЛЕ саме в цю мілісекунду поп-ап з'являється на екрані, перекриває кнопку, і ваш тест падає з помилкою Element is intercepted.Ви перезапускаєте тест — сервер відповідає швидше, поп-ап з'являється миттєво, if спрацьовує, тест "зелений". Вітаю, ви створили еталонний Flaky-тест!
✅ Як це виглядає після код-рев'ю Senior-інженера:Тест має бути прямою лінією. Якщо банер можна відключити (через cookie або API-мок) — відключаємо. Якщо ж це неконтрольований поп-ап, використовуємо нативну "магію" Playwright 1.42+:// Ідеально чистий код (Playwright addLocatorHandler)test('Повинен додати товар у кошик', async ({ page }) => { // 1. Вчимо Playwright автоматично реагувати на перешкоду, ЯКЩО вона з'явиться await page.addLocatorHandler( page.locator('.promo-popup'), async () => { await page.locator('.close-promo').click(); } ); await page.goto('/product/123'); // 2. Тест залишається абсолютно лінійним, ніяких if/else! await page.locator('.add-to-cart').click();});
Золоте правило: Ніколи не використовуйте if / else для синхронізації UI чи обробки випадкових елементів на сторінці. Автотест — це не алгоритм пошуку шляху, це жорсткий сценарій. Контролюйте стан додатка, а для асинхронних перешкод делегуйте роботу фреймворку.А скільки if'ів зараз заховано у вашому фреймворку? 👇🔥 — Використовую addLocatorHandler, мої тести прямі як стріла!👀 — Грішу іф-елсами, бо на проді постійно лізуть рандомні банери...🤯 — Тепер я зрозумів, чому мої тести періодично падають на кліках!
💩 Код з душком: Перехресне запилення, або Чому ваші тести падають у CI/CDПривіт, екіпаж! П'ятниця — час для генерального прибирання у ваших репозиторіях. Сьогодні ми препаруємо одну з найнебезпечніших інженерних хвороб — sharing state (обмін станом) між тестами. Це той випадок, коли ви використовуєте глобальні змінні для передачі даних від одного тесту до іншого. ☕️Знайдіть проблему в цьому коді:// ❌ Як пишуть джуни (Антипатерн "Глобальний наркоман")// Десь у глобальному скоупі файлуlet lastCreatedUserId: string;test('Тест 1: Створити юзера', async ({ request }) => { const newUser = await request.post('/api/users', { data: { name: 'Bro' } }); // Зберігаємо ID у глобальну змінну для наступного тесту lastCreatedUserId = (await newUser.json()).id;});test('Тест 2: Видалити створеного юзера', async ({ request }) => { // Використовуємо ID, який створив ПОПЕРЕДНІЙ тест await request.delete(`/api/users/${lastCreatedUserId}`);});
Чому цей код тхне:На вашій локальній машині це може працювати. Але в CI/CD Playwright за замовчуванням запускає тести паралельно (в різних воркерах/процесах).Коли воркер №2 почне виконувати «Тест 2», глобальна змінна lastCreatedUserId у його процесі буде порожньою (undefined), бо «Тест 1» виконувався в іншому воркері №1. У вас "червоний" пайплайн, а ви витрачаєте пів дня, намагаючись зрозуміти, чому тест на видалення не бачить ID.
✅ Як це виглядає після код-рев'ю Senior-інженера:// Ідеально чистий код (Повна атомарність)test('Повинен видаляти користувача', async ({ request }) => { // 1. Створюємо юзера ІЗОЛЬОВАНО всередині одного тесту const newUser = await request.post('/api/users', { data: { name: 'IsolatedBro' } }); const userId = (await newUser.json()).id; // 2. Використовуємо ID тут же await request.delete(`/api/users/${userId}`);});
Золоте правило: Кожен тест має бути повністю атомарним. Тест має сам створювати потрібні дані, виконувати дію і сам за собою прибирати (якщо це необхідно). Жодних глобальних змінних для передачі даних!А ваші тести бігають у паралель, чи ви боїтесь, що вони "перепиляться" даними? 👇🔥 — Тільки атомарні тести, тільки паралельний запуск!👀 — Грішимо глобальними змінними, тому запускаємо по одному...🤯 — Мої тести впали вчора, тепер я знаю чому!
🔍 Рентген співбесіди: «Ми вам передзвонимо» або що насправді шукає рекрутерБуває так: ти розклав по поличках роботу Playwright, пояснив різницю між Promise.all та Promise.allSettled, і навіть задизайнив фреймворк на ходу. Але в кінці отримуєш стандартне «дякуємо, ми на зв'язку».Давайте «просвітимо» рентгеном, які приховані патології в софт-скілах або технічних відповідях найчастіше стають причиною відмови.🦴 «Скелет в шафі» технічного боргуКоли тебе питають про архітектуру, а ти розповідаєш лише про те, як писати селектори. Рекрутер бачить не Middle AQA, а людину, яка просто автоматизує ручні кейси без розуміння стратегії.🔹Діагноз: Відсутність системного мислення.🔹Лікування: Говори про CI/CD, звіти, інтеграцію з Test IT та обробку флекі-тестів за допомогою ретраїв.
🧪 «Магічне» тестування (No-Code ілюзія)Якщо ти занадто сильно покладаєшся на ШІ-інструменти або no-code рішення, не розуміючи, як вони працюють «під капотом», на рентгені це виглядає як купа іржавих шестерень, заклеєних синьою ізолентою.🔹Діагноз: Низька технічна експертиза.🔹Лікування: Покажи, що ти знаєш DOM-дерево Angular краще за будь-який дрон, і можеш написати чистий код навіть на серветці.
🧬 Ізоляція замість інтеграціїКандидат ідеально знає свій «чорний ящик» API, але поняття не має, як дані потрапляють з фронтенда в БД або як працює аутентифікація через Bearer токени.🔹Діагноз: Обмежений кругозір.🔹Лікування: Вивчай DevTools як свої п'ять пальців — від нетворк-графів до спуфінгу геолокації.
🚩 Red Flags на знімку:
🔹Хардкод таймаути: Якщо на питання про очікування ти кажеш await page.waitForTimeout(5000), десь у світі сумує один лід.🔹Ігнорування витоків пам'яті: Тести проходять, але браузер «зжирає» всю RAM на сервері? Рентген покаже це як «цифрових привидів», що душать твоє залізо.
Порада дня: Співбесіда — це не допит, а перевірка сумісності твого «коду» з культурою команди. Будь як той архітектор з майбутнього: спокійним, впевненим і завжди з чітким планом автоматизації.А які «діагнози» ставили вам на співбесідах? Пишіть у коментарях! 👇#QA #AQA #Testing #Interview #Career #Playwright #Angular
🗂 Ультимативна шпаргалка: Локатори Playwright (2018 vs 2026)Коротка шпаргалка про те, як відучити себе писати "крихкі" тести. Зберігайте в обране і кидайте на код-рев'ю. ☕️❌ Застарілий підхід (CSS / XPath) ✅ Сучасний підхід (User-Facing API)1️⃣ Кнопки та посилання
❌ page.locator('.btn-primary.submit')✅ page.getByRole('button', { name: 'Зберегти' }) Чому: Перевіряє не лише текст, а й доступність (Accessibility). Якщо розробник випадково зробить кнопку div-ом, тест впаде.2️⃣ Пошук статичного тексту
❌ page.locator('//div/span[contains(text(), "Успіх")]')✅ page.getByText('Успіх', { exact: true }) Чому: Працює незалежно від того, як фронтендер перетасує div та span у компоненті.3️⃣ Специфічні блоки (картки, аватари)❌ page.locator('.user-card .avatar-img')✅ page.getByTestId('user-avatar')
Чому: CSS-класи належать дизайнерам (вони їх змінюють). Атрибути data-testid належать QA — їх ніхто не чіпає.4️⃣ Точковий пошук у списках/таблицях (Chaining)❌ page.locator('.table-row:has-text("Ivan") >> .delete-btn')✅ page.getByRole('row', { name: 'Ivan' }).getByRole('button', { name: 'Видалити' })
Чому: Читається як звичайна англійська мова, нуль магії.Золоте правило: Шукайте елементи так, як їх шукає реальний юзер (за текстом та роллю), а не так, як їх бачить браузер (за селекторами).А на чому сидите ви? 👇🔥 — Тільки getByRole та getByText, класика.👀 — Мій бестфренд — це data-testid.🤬 — Я фанат XPath, мене вже не перевчити!