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

    06 · Автоматизація: стратегія

    Анатомія автотесту: AAA, незалежність, атомарність

    Зміст

    Зелений тест сам по собі не вартий нічого — цінність має тест, якому можна вірити. А довіра народжується не з магії фреймворка, а зі структури: коли тест падає, ти маєш за секунди зрозуміти, що саме зламалося, не запускаючи його вдесяте й не читаючи двісті рядків. Погано влаштований тест — це той, що падає з повідомленням «element not found», а справжня причина за три екрани вище, у чужому тесті, який не прибрав за собою.

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

    Три фази: Arrange–Act–Assert

    Arrange–Act–Assert (AAA) — це шаблон структури тіла тесту в три фази: підготувати (arrange), виконати цільову дію (act), перевірити наслідок (assert). Ідея стара, з ери : споріднену модель Джерард Мезарос (Gerard Meszaros) описує як (four-phase test) — setup (≈arrange), exercise (≈act), verify (≈assert) і додаткова teardown для . Спільну підготовку й прибирання зазвичай виносять у (fixtures) й хуки (див. Тестовий стек зсередини).

    Setup
    фікстура / хук

    Arrange
    дані й стан

    Act
    одна цільова дія

    Assert
    перевірка наслідку

    Teardown
    прибрати за собою

    Setup
    фікстура / хук

    Arrange
    дані й стан

    Act
    одна цільова дія

    Assert
    перевірка наслідку

    Teardown
    прибрати за собою

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

    test('кошик показує кількість доданих товарів', async ({ page }) => {
      // Arrange
      await page.goto('/catalog');
    
      // Act
      await page.getByRole('button', { name: 'Додати в кошик' }).first().click();
    
      // Assert
      await expect(page.getByTestId('cart-badge')).toHaveText('1');
    });

    Одне цільове Act — ключовий момент. Якщо у фазі дії три кліки й дві навігації, тест перевіряє вже не одну поведінку, а сценарій, і причин впасти стає багато. Споріднений формат — Given–When–Then з BDD: те саме розбиття, але словами домену (мета — , а не структура коду; див. BDD і Gherkin).

    Одна причина впасти: атомарність

    Атомарність (atomicity) тесту — це властивість «одна причина впасти». Тест перевіряє один інваріант (invariant) — одну властивість системи, яка має справджуватися, — і якщо він червоний, причина очевидна з назви.

    Тут живе поширений міф, який варто одразу зняти: атомарність — це не «рівно один assert на тест». Перевірити один інваріант часто означає кілька , які разом описують один стан:

    test('після оплати замовлення переходить у статус paid', async ({ page }) => {
      // ... Arrange + Act ...
    
      // усе це — про ОДИН інваріант: «замовлення оплачене»
      await expect(page.getByTestId('order-status')).toHaveText('Оплачено');
      await expect(page.getByTestId('pay-button')).toBeHidden();
      await expect(page.getByTestId('receipt-link')).toBeVisible();
    });

    Антипатерн — не кількість асертів, а кількість не повʼязаних причин. Коли один тест логіниться, створює товар, редагує профіль і перевіряє оформлення замовлення — він упаде з десятка різних причин, і кожен фейл вимагатиме розслідування «а на якому саме кроці?». Розбийте такий тест на кілька атомарних: логін окремо, створення окремо, оформлення замовлення окремо. Бонус — вони зможуть бігти паралельно.

    Атомарність напряму годує ще один інструмент — hard vs soft асерти (нижче): у більшості випадків тест має падати на першій же розбіжності, бо решта асертів у зламаному стані все одно неінформативні.

    Незалежність: тест не має памʼятати про сусідів

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

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

    Незалежні: кожен готує свій стан

    test A: свій товар

    test B: свій товар

    test C: свій товар

    Зчеплені: падають від порядку

    test A: створює товар

    test B: очікує товар від A

    test C: видаляє товар

    Незалежні: кожен готує свій стан

    test A: свій товар

    test B: свій товар

    test C: свій товар

    Зчеплені: падають від порядку

    test A: створює товар

    test B: очікує товар від A

    test C: видаляє товар

    У зчепленому варіанті досить, щоб test A впав або щоб ранер переставив тести — і test B червоний не через баг, а через сусіда. У незалежному кожен тест сам готує собі стан і сам його прибирає.

    Джерела зчеплення, яких треба уникати: спільні змінні на рівні файлу, порядок як прихована умова, і — найпідступніше — спільний стан у застосунку (один тестовий користувач на всіх, спільний запис у БД). Playwright дає ізоляцію майже даром: кожен тест отримує свіжий browser context — окремий профіль без кук і localStorage попереднього тесту. Але ізоляція браузера не рятує від спільного стану на бекенді: якщо два паралельні тести правлять того самого юзера, конфлікт неминучий (детально — Тест-дані).

    Підготовка стану через API або БД

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

    Правило: готуй стан найдешевшим і найнадійнішим каналом, а не через UI. Зазвичай це прямий , рідше — запис у базу. Чому:

    • Швидкість. Один POST-запит замість десятка кліків з очікуваннями.
    • Стабільність. Підготовка не залежить від верстки, анімацій і чужих екранів.
    • Чесність фейлу. Якщо реєстрація через API успішна, а тест профілю впав — виною профіль, а не setup. UI-підготовка змішує ці причини й порушує атомарність.
    test('користувач бачить своє імʼя в профілі', async ({ page, request }) => {
      // Arrange: стан через API, а не через UI-реєстрацію
      const res = await request.post('/api/users', {
        data: { email: `u+${Date.now()}@example.com`, name: 'Ihor' },
      });
      const { id, token } = await res.json();
    
      // Act
      await page.goto(`/profile/${id}?token=${token}`);
    
      // Assert
      await expect(page.getByRole('heading', { level: 1 })).toHaveText('Ihor');
    });

    Виняток лише один: коли перевіряєш саме UI-флоу підготовки — тоді реєстрація через форму є предметом тесту, і скорочувати її не можна. У всіх інших випадках UI-шлях у Arrange — це технічний борг, який колись стане . через API vs напряму в БД — окремий вибір: API поважає бізнес-правила й тригери, прямий запис у БД швидший, але легко лишає систему в неможливому стані (детальніше в темах про API-тестування й SQL).

    Hard проти soft асертів

    За замовчуванням асерти жорсткі (hard): перший невдалий expect кидає виняток і зупиняє тест. Це правильний дефолт — після зламаного інваріанту решта асертів часто в непередбачуваному стані, і їхній вердикт лише шумить.

    Мʼякі (soft) асерти не зупиняють тест: усі розбіжності збираються, тест доходить до кінця й падає наприкінці зі списком усього, що не так. У Playwright це expect.soft(...):

    test('картка товару відрендерена повністю', async ({ page }) => {
      await page.goto('/product/42');
    
      await expect.soft(page.getByTestId('title')).toHaveText('Термокухоль');
      await expect.soft(page.getByTestId('price')).toHaveText('₴499');
      await expect.soft(page.getByTestId('stock')).toHaveText('В наявності');
    });

    Коли що: hard — коли наступні кроки не мають сенсу без попереднього (немає кнопки «Оплатити» — нема що й клікати далі). soft — коли перевіряєш незалежні аспекти одного стану й хочеш побачити всі розбіжності за один прогін, а не лагодити їх по одній. Класичний кейс для soft — валідація багатьох полів відрендереної сторінки чи тіла API-відповіді.

    Пастка soft: після нього тест продовжується, тож якщо далі йде дія, що залежить від невдалої перевірки, отримаєш незрозумілий каскадний фейл. Soft хороший наприкінці фази Assert, а не посеред взаємозалежних кроків.

    Назва описує інваріант, а не дію

    Назва тесту — перше, що ти бачиш у червоному звіті, часто взагалі без коду поруч. Тому вона має відповідати на питання «яку властивість системи ми втратили?», а не переказувати кроки.

    Погана назваЧому поганоДобра назва
    test('клік по кнопці')Описує дію, не результатгість не може видалити чужий коментар
    test('перевірка форми')Ні предмета, ні очікуванняформа логіну відхиляє невалідний email
    test('тест 3')Нічого не говоритьпорожній кошик показує заглушку

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

    Тест як документація

    Складіть докупи AAA, атомарність і назву-інваріант — і тест починає працювати як жива документація поведінки. Новий інженер відкриває сюїту й читає її як перелік гарантій продукту: «гість бачить заглушку порожнього кошика», «оплата переводить замовлення в paid», «невалідний email відхиляється». Це документація, яка не бреше, бо CI не дасть їй розійтися з кодом.

    Декларативний стиль підсилює ефект — коли тест говорить що, а не як. У CodeceptJS сценарний синтаксис читається майже як текст вимоги:

    Scenario('гість не бачить кнопку видалення статті', ({ I }) => {
      I.amOnPage('/article/10');        // Arrange
      I.dontSeeElement('[data-testid="delete-article"]'); // Assert
    });

    Умова, щоб це працювало: назви — інваріантами, тіло — коротким і на одному рівні абстракції, а деталі локаторів заховані в (див. Page Object) чи кроки. Тест, у якому впереміш CSS-селектори, sleep і бізнес-логіка, документацією не стане — його самого треба документувати.

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

    • Виглядає як «один assert = атомарність», а насправді атомарність — це «одна причина впасти». Розбивати повʼязані перевірки одного стану на окремі тести зайве; шкідливо змішувати не повʼязані причини в один тест.
    • Виглядає як незалежний тест, а насправді він тримається на сусідові. Немає явного порядку в коді, але test B очікує дані, які створив test A. Локально й послідовно зелено; у паралелі чи при перемішуванні — випадковий .
    • Виглядає як «підготували дані», а насправді протестували UI-реєстрацію двадцятий раз. Setup через ту саму форму робить кожен тест заручником чужого екрана й ховає справжню причину фейлу.
    • Виглядає як ретельна перевірка, а насправді каскад від soft. Після невдалого expect.soft тест іде далі й падає на дії, що залежала від зламаного стану, — репорт вказує не туди.
    • Виглядає як зайвий teardown, а насправді витік стану. Тест створив запис і не прибрав — наступний прогін ловить дубль або конфлікт унікальності. Прибирання за собою — частина незалежності, а не формальність.
    • Виглядає як «тест проходить, отже все добре», а насправді назва бреше. test('перевірка кошика') зелений, але що саме він гарантує — не скаже ніхто, і він пропустить мовчки.

    Підсумок

    • AAA розбиває тіло тесту на підготовку, одну цільову дію й перевірку наслідку — це робить намір читабельним, а фейл локалізованим.
    • Атомарність — «одна причина впасти», а не «один assert»: кілька асертів про один інваріант нормальні, кілька не повʼязаних причин в одному тесті — ні.
    • Незалежність від порядку — паралельного запуску; спільний стан (на бекенді, не лише в браузері) — головне джерело флаку.
    • Стан у Arrange готуй через API чи БД, а не через UI, — крім випадку, коли сам UI-флоу і є предметом тесту.
    • Hard — дефолт (падати на першій розбіжності); soft — для незалежних перевірок одного стану наприкінці Assert. Назва описує інваріант, і тоді сюїта читається як жива документація.

    Можливі питання

    • «Що таке AAA і навіщо?» — інтервʼюер перевіряє, чи розумієте ви, що структура потрібна для діагностованості фейлу, а не для краси. Слабка відповідь переказує три слова; сильна додає «одне цільове Act» і згадує setup/teardown у фікстурах.
    • «Чому тести мають бути незалежними? Що станеться, якщо ні?» — очікують звʼязок із паралелізацією й флаком: у паралельному прогоні порядок недетермінований, тож зчеплені тести падають випадково. Плюс приклад джерела зчеплення (спільний користувач, дані від сусіда).
    • «Скільки asserts має бути в тесті — один чи багато?» — пастка на догму. Правильно — «стільки, скільки описують один інваріант; погано не багато асертів, а багато не повʼязаних причин впасти».
    • «Як готуєте тестові дані — через UI чи інакше? Чому?» — дивляться, чи знаєте ви про підготовку через API/БД і чому вона надійніша й швидша, і чи бачите виняток (коли UI-флоу — сам предмет тесту).
    • «hard vs soft assertions — коли що?» — очікують не лише синтаксис expect.soft, а критерій вибору: залежні кроки → hard, незалежні аспекти одного стану → soft, і пастку каскадного фейлу після soft.
    • «Як назвати тест?» — сигнал зрілості: назва як інваріант/речення специфікації, а не переказ кліків.

    Джерела

    Три фази: Arrange–Act–Assert

    • xUnit Test Patterns — Four-Phase Test — чотири послідовні частини (fixture setupexercise SUTresult verificationfixture teardown) і мета поділу: щоб читач швидко бачив, яку поведінку перевіряє тест; винесення setup/teardown у хуки — штатний варіант.
    • Martin Fowler — GivenWhenThen — Given/When/Then як переформулювання чотирифазного тесту; там же атрибуція формулювання «Arrange, Act, Assert» Біллу Вейку і причина, чому в BDD четвертої фази немає.

    Одна причина впасти: атомарність

    • xUnit Test Patterns — Four-Phase Test — краще багато маленьких тестів на одну умову, ніж один великий, і причина не естетична, а ; практичний маркер порушення — кілька фаз дії, розділених перевірками.
    • xUnit Test Patterns — Obscure Test — «жадібний тест» (Eager Test) перевіряє забагато в одному методі, і падіння одного асерту ховає решту помилок, які просто не виконаються.
    • ISTQB® CTAL-TAE Syllabus v2.0 — §4.2: саме фікстури уможливлюють повторювані й атомарні автотести.

    Незалежність: тест не має памʼятати про сусідів

    • Playwright — Best Practices — кожен тест має бути повністю ізольований і виконуватися незалежно: власні localStorage, sessionStorage, дані й кукі.
    • Playwright — Test Isolation (browser contexts) — три названі наслідки ізоляції (немає перенесення падіння, легше дебажити, не треба думати про порядок у паралелі та ) і дві стратегії: починати з нуля або прибирати між тестами.
    • xUnit Test Patterns — Erratic Test — маркер саме цієї причини: тест сам по собі зелений, а в сюїті падає при додаванні або вилученні іншого тесту; корінь — спільна фікстура.
    • xUnit Test Patterns — Fresh Fixture — свіжа фікстура на кожен тест запобігає нестабільним тестам.

    Підготовка стану через API або БД

    • Playwright — API testing — підготовка серверного стану перед відкриттям застосунку названа офіційно підтримуваним застосуванням API-контексту, а стан автентифікації переноситься між API- і браузерним контекстом через storageState().
    • ISTQB® CTFL Syllabus v4.0.1 — §1.4.3–1.4.4, §4.1: підготовка даних і середовища — планована активність , а не імпровізація перед прогоном.

    Hard проти soft асертів

    • Playwright — Test assertions — мʼякий асерт (expect.soft) виконання тесту не зупиняє, але позначає його проваленим; звичайний асерт за замовчуванням зупиняє.
    • ISTQB® CTAL-TAE Syllabus v2.0 — §6.1.1: статус має бути визначений коректно, а в разі failed потрібна додаткова інформація про причину.

    Назва описує інваріант, а не дію

    • Dan North — Introducing BDD — «test method names should be sentences»: виразна назва працює саме в момент падіння, бо звітує про втрачену поведінку без читання коду.

    Тест як документація

    • xUnit Test Patterns — Obscure Test — тест служить двом цілям (документація поведінки й самоперевірна виконувана специфікація), і ці цілі часто суперечать одна одній.
    • Cucumber Docs — Writing better Gherkin — звідки взято сам термін «жива документація» і чому декларативний стиль («що», а не «як») її уможливлює.

    Пояснення

    «Поясни» працює з власним API-ключем Anthropic: запит іде з вашого браузера прямо до Anthropic.

    Свій API-ключ (BYOK)

    Вставте власний ключ Anthropic — пояснення працюватиме на реальній моделі. Ключ зберігається лише у вашому браузері (localStorage), нікуди не надсилається, крім api.anthropic.com, і не логується.