Переходи станів
Зміст
Класи еквівалентності й граничні значення працюють з окремим входом, таблиці рішень — з комбінацією умов у конкретний момент. Але чимала частина багів живе не в моменті, а в часі: система реагує на ту саму дію по-різному залежно від того, що з нею відбувалося раніше. Кнопка «Оплатити» на новому замовленні списує гроші, а на скасованому її взагалі не має бути. Той самий запит, той самий користувач — різний очікуваний результат, бо різна історія.
Тестування переходів станів (state transition testing) — техніка саме для таких фіч. Вона змушує виписати всі стани системи і всі події, а потім чесно відповісти на незручне питання: «а що буде, якщо зробити це не в тому порядку?» Більшість продуктових вимог описують лише щасливий шлях, тому баги «не в тому порядку» — оплата скасованого, редагування опублікованого, повторне надсилання підтвердженого — систематично проскакують повз перевірки, спроєктовані іншими техніками.
Чотири елементи моделі
Модель поведінки в часі складається з чотирьох речей:
- Стан (state) — режим, у якому система по-різному реагує на ті самі події. Замовлення «нове» і «скасоване» — різні стани, бо подія «оплатити» дає різний результат.
- Подія (event) — те, що трапляється: дія користувача, відповідь платіжної системи, таймаут, повідомлення з черги. Подія може мати сторожову умову (guard condition): «оплатити» спрацьовує, лише якщо сума більша за нуль.
- Перехід (transition) — зміна стану у відповідь на подію: «нове» після успішної оплати стає «оплаченим».
- Дія (action) — те, що система робить під час переходу, крім зміни самого стану: надіслати лист, повернути кошти, записати подію в аудит-лог.
Найважливіше розрізнення тут — стан проти атрибута. Замовлення на 100 грн і на 5000 грн — це не два стани, це два значення поля. Станом варто вважати лише те, що змінює реакцію системи на подію. Якщо тягти в модель усі атрибути, машина розростеться до сотень «станів», і техніка помре під власною вагою. Тест на модель простий: «чи відповість система на цю подію інакше?» Ні — це не окремий стан.
Класичний життєвий цикл замовлення виглядає так:
Підпис на стрілці читається як «подія / дія»: скасування оплаченого замовлення не просто змінює стан — воно ще й запускає повернення коштів. Саме дії найчастіше губляться у вимогах: перехід описали, а що при цьому має статися з грошима і листами — ні.
Діаграма чи таблиця переходів
Діаграма станів — це погляд «що система робить». Вона наочна, її зручно малювати на дзвінку з аналітиком і класти в документацію. Але в діаграми є сліпа пляма: на ній видно лише ті переходи, які хтось не забув намалювати. Чого на стрілках немає — того ніби й не існує.
Таблиця переходів (state table) закриває цю пляму механічно: рядки — стани, колонки — події, і кожна клітинка вимагає відповіді.
| Стан \ Подія | оплата | скасування | відправлення | доставлення |
|---|---|---|---|---|
| Нове | Оплачене | Скасоване | — | — |
| Оплачене | — | Скасоване | Відправлене | — |
| Відправлене | — | — | — | Доставлене |
| Доставлене | — | — | — | — |
| Скасоване | — | — | — | — |
П'ять станів і чотири події дають 20 клітинок. Валідних переходів — лише 5. Решта 15 «прочерків» — це і є головна цінність таблиці: кожен прочерк — питання «а що система зробить, якщо ця подія все-таки прилетить?»
| Аспект | Діаграма | Таблиця |
|---|---|---|
| Що показує | Валідні переходи, потік | Усі комбінації стан і подія |
| Невалідні переходи | Невидимі | Явні (порожні клітинки) |
| Найкраще для | Комунікації, розуміння фічі | Пошуку дірок, повноти покриття |
На практиці порядок такий: діаграму береш (або малюєш) для розуміння, таблицю будуєш для дизайну тестів. Це та сама логіка, що й у таблицях рішень: змусити себе перебрати всі комбінації, а не лише ті, що спали на думку.
Невалідні переходи
Невалідний перехід — комбінація стану і події, якої за моделлю бути не повинно: оплатити скасоване, скасувати доставлене, відправити неоплачене. Дві речі роблять їх обов'язковою частиною тест-дизайну, а не факультативом.
Перша: «кнопки немає в UI» не означає «перехід неможливий». Подія може прилетіти повз інтерфейс — прямим запитом до API, з другої вкладки браузера, зі сторінки, відкритої до зміни стану, після кнопки «назад». Класика: користувач відкрив замовлення у двох вкладках, в одній скасував, у другій натиснув «Оплатити» — сторінка ж стара, кнопка на ній ще є. Той самий механізм — протухла сесія: вкладка показує кабінет, хоча сесії давно немає (докладно — у главі про кукі й сесії). Тому невалідні переходи перевіряють мінімум на двох рівнях: UI не пропонує дію, а бекенд відхиляє її, навіть якщо запит прийшов.
Друга: для невалідного переходу теж має бути визначена очікувана поведінка. «Система ігнорує», «повертає помилку з поясненням», «показує актуальний стан» — це різні реакції, і яка з них правильна, мають сказати вимоги. Якщо вимоги мовчать — це знахідка ще до першого тесту: таблиця переходів тут працює як інструмент рев'ю вимог, про який ішлося в першій главі розділу.
Рівні покриття
«Протестувати машину станів» — не бінарне питання. Є градації, і на співбесіді очікують, що ти їх розрізняєш:
| Рівень | Що гарантує | Ціна |
|---|---|---|
| Усі стани (all states) | Кожен стан відвіданий хоч раз | Найдешевше, найслабше |
| Усі валідні переходи (valid transitions, 0-switch) | Кожна стрілка діаграми пройдена хоч раз | Практичний мінімум |
| Усі переходи (all transitions) | Валідні плюс реакція на кожен невалідний | Для критичних фіч |
Покриття рахується як частка перевірених елементів від усіх елементів рівня. Пастка в тому, що «усі стани» звучить солідно, але майже нічого не гарантує: у нашому замовленні всі п'ять станів можна відвідати одним ланцюжком плюс одним скасуванням, пропустивши, наприклад, перехід «Оплачене → Скасоване» — а саме на ньому живе повернення коштів.
Покриття всіх валідних переходів — найуживаніший критерій і практичний мінімум. Зверни увагу: кількість тестів не дорівнює кількості переходів, бо один тест-ланцюжок покриває кілька стрілок поспіль. Наші 5 валідних переходів закриваються трьома ланцюжками:
- Нове → Оплачене → Відправлене → Доставлене (3 переходи);
- Нове → Скасоване (1 перехід);
- Нове → Оплачене → Скасоване (1 новий перехід, з перевіркою повернення коштів).
Рівень «усі переходи» додає до цього 15 перевірок невалідних комбінацій — стільки, скільки прочерків у таблиці. Для платіжного флоу це виправдано, для перемикача теми оформлення — навряд: обсяг покриття — питання ризику.
Є й глибші рівні: покриття послідовностей із двох і більше переходів поспіль (у літературі — 1-switch, n-switch coverage). Вони ловлять баги, які проявляються лише в певній послідовності: скасування працює, повторне замовлення працює, а «скасував і одразу замовив знову» — падає. На рівні джуна досить знати, що такі рівні існують і навіщо.
State machine у реальних фічах
Техніка здається академічною рівно доти, доки не помітиш, що машини станів — усюди. Маркери у вимогах: слова «статус», «етап», «життєвий цикл», «воркфлоу». Типові місця:
- Замовлення, платежі, доставка — канонічний приклад вище; у платіжних систем ще й статуси на боці провайдера.
- Підписки: тріал → активна → прострочена оплата → скасована; переходи запускають листи, зміну доступів, повторні списання.
- Документообіг: чернетка → на погодженні → опубліковано → в архіві; «відредагувати опубліковане» — типовий невалідний перехід.
- Задача в трекері: воркфлоу в Jira — буквально конфігурована машина станів з дозволеними переходами між статусами.
- Обліковий запис і сесія: анонімний → залогінений → заблокований; сесія жива → протухла.
- UI-компоненти: кнопка звичайна → у стані завантаження → заблокована; повторний клік у стані завантаження — це подія в невалідному стані, і подвійне створення замовлення — її типовий наслідок.
Практичний бонус: щойно фіча описана як машина станів, з неї «безкоштовно» випадають питання до аналітика. Що буде з коштами при скасуванні відправленого? Чи може адмін перевести доставлене назад у відправлене? Половина цих питань не має відповіді у вимогах — і краще з'ясувати це до розробки, ніж багом на проді.
Переходи станів у e2e
Для автотестів модель станів — це не лише джерело кейсів, а й спосіб думати про структуру сюїти.
E2e-сценарій — це шлях через машину станів. Смоук — найкоротший щасливий шлях (ланцюжок 1 з розрахунку вище). Повна регресія — набір шляхів, який закриває обраний рівень покриття. Коли покриття сформульоване в термінах переходів, легко аргументувати, чому тестів саме стільки.
Стартовий стан готуй напряму, а не прокликуванням. Тест переходу «Відправлене → Доставлене» не повинен щоразу проходити через UI створення, оплату й відправлення: це три зайві точки відмови і зайвий час. Стан «відправлене» створюється через API або сідинг даних, а тест перевіряє рівно один перехід. Один довгий тест через увесь життєвий цикл — антипатерн: падіння на другому кроці ховає результати всіх наступних.
Невалідні переходи перевіряй на обох рівнях. UI-перевірка (дії немає) і API-перевірка (запит відхиляється) — це два різні тести з різними багами:
test('скасоване замовлення не можна оплатити', async ({ request, page }) => {
// стартовий стан — через API, не через UI-ланцюжок
const order = await createOrderViaApi(request, { status: 'cancelled' });
// невалідний перехід відхиляється бекендом
const res = await request.post(`/api/orders/${order.id}/pay`);
expect(res.status()).toBe(409);
// і не пропонується в UI
await page.goto(`/orders/${order.id}`);
await expect(page.getByRole('button', { name: 'Оплатити' })).toBeHidden();
});
Тут статус 409 Conflict — типова відповідь на подію в несумісному стані, але конкретний контракт (409, 422, тихе ігнорування) диктують вимоги, а не звичка.
Кожен тест мусить знати свій стартовий стан. Найпідступніший флак у стейтфул-фічах — тест, який неявно припускає стан, залишений попереднім тестом. Ізоляція даних і незалежність тестів — тема розділу про стратегію автоматизації, але корінь проблеми видно вже тут: тест без явного стартового стану — це перехід із невідомого стану, і його результат невідомий за означенням.
Типові помилки
- Виглядає як стан, а насправді атрибут. У модель тягнуть суму, місто доставки, спосіб оплати — і отримують «машину» на сотню станів. Стан — лише те, що змінює реакцію системи на подію; решта — тестові дані для інших технік.
- Виглядає як повне покриття, а насправді лише всі стани. «Я пройшов усі статуси» звучить переконливо, але відвідати всі стани можна, не перевіривши й половини переходів — і саме на непройденому переході живе повернення коштів.
- Виглядає як неможливий перехід, а насправді — недоступний з UI. Кнопку сховали, а API приймає запит; або друга вкладка зі старою сторінкою робить «неможливе» одним кліком. Модель описує систему, а не один її інтерфейс.
- Виглядає як економія — один тест на весь життєвий цикл, а насправді ланцюжок доміно. Падіння на другому переході ховає результат п'яти наступних, а кожен ретест починається з нуля. Один тест — один перехід (або короткий ланцюжок), стартовий стан — через API.
- Виглядає як повна модель, а насправді happy path з вимог. Діаграма з документації показує, що намалював аналітик. Повноту дає лише таблиця: кожна порожня клітинка — це або свідоме «ігноруємо», або дірка у вимогах.
Підсумок
- Стан — це те, що змінює реакцію системи на ту саму подію; усе інше — атрибути даних, а не стани.
- Діаграма показує валідну поведінку і годиться для комунікації; таблиця переходів перебирає всі комбінації стан і подія та робить невалідні переходи видимими.
- Практичний мінімум покриття — усі валідні переходи; «усі стани» — слабша гарантія; невалідні переходи додають за рівнем ризику фічі.
- Невалідний перехід перевіряється не лише в UI: API, друга вкладка, застаріла сторінка — усе це шляхи, якими подія обходить інтерфейс.
- В e2e один тест перевіряє один перехід зі стартовим станом, підготовленим через API; довгий ланцюжок через увесь цикл — антипатерн.
Що питають на співбесіді
- «Поясни техніку переходів станів. Чим вона відрізняється від таблиці рішень?» Очікувана суть: таблиця рішень — комбінації умов в один момент часу, переходи станів — поведінка, що залежить від історії. Інтерв'юер дивиться, чи розумієш, коли яку техніку брати, а не завчив означення.
- «Що таке невалідний перехід і як його тестувати?» Сильна відповідь згадує таблицю станів як спосіб їх знайти і перевірку поза UI (API, паралельні вкладки). Якщо кандидат сам, без підказки, згадує невалідні переходи — для інтерв'юера це маркер зрілості.
- «Скільки тестів потрібно, щоб покрити цю діаграму?» Пастка без уточнення: правильна реакція — спитати, який рівень покриття потрібен, і показати, що один ланцюжок покриває кілька переходів. Дивляться на структуру міркування, не на число.
- «Наведи приклад машини станів із продукту, який ти тестував.» Чекають конкретики: замовлення, підписка, воркфлоу задачі — зі станами і хоча б одним невалідним переходом, який ти реально перевіряв.
- Практичне завдання: дають діаграму (банкомат, документообіг) і просять спроєктувати тести. Виграшна структура: таблиця переходів → валідні ланцюжки → невалідні комбінації за ризиком.
Джерела
- ISTQB CTFL Syllabus v4.0 — розділ 4.2.4 «State Transition Testing»: означення, таблиця станів, рівні покриття.
- Глосарій ISTQB — канонічні означення state transition testing, valid/invalid transition, coverage.
- Playwright: API testing — підготовка стартового стану та перевірка переходів через API з e2e-тестів.
- Playwright: Browser contexts — ізоляція тестів, без якої стартовий стан не гарантований.
Що таке тестування переходів станів і коли цю техніку варто застосовувати?
Це техніка тест-дизайну для фіч, де реакція системи на ту саму дію залежить від того, що з нею відбувалося раніше: кнопка «Оплатити» на новому замовленні списує гроші, а на скасованому її взагалі не має бути. Ти виписуєш усі стани системи й усі події, а потім перевіряєш, що відбувається в кожній комбінації — включно з тими, яких «не повинно бути». Застосовувати варто там, де у вимогах з'являються маркери «статус», «етап», «життєвий цикл», «воркфлоу»: замовлення й платежі, підписки, документообіг, воркфлоу задач у трекері, обліковий запис і сесія. Головна цінність техніки в тому, що вона змушує відповісти на питання «а що буде, якщо зробити це не в тому порядку?» — більшість вимог описують лише щасливий шлях, тому баги «оплата скасованого» чи «редагування опублікованого» систематично проскакують повз перевірки, спроєктовані іншими техніками.
З яких елементів складається модель переходів станів?
Чотири елементи: стан, подія, перехід, дія. Стан (state) — режим, у якому система по-різному реагує на ті самі події: «нове» й «скасоване» замовлення — різні стани, бо подія «оплатити» дає різний результат. Подія (event) — те, що трапляється: дія користувача, відповідь платіжної системи, таймаут, повідомлення з черги; подія може мати сторожову умову (guard condition) — «оплатити» спрацьовує, лише якщо сума більша за нуль. Перехід (transition) — зміна стану у відповідь на подію, а дія (action) — те, що система робить під час переходу, крім зміни самого стану: надіслати лист, повернути кошти, записати в аудит-лог. У вимогах найчастіше губляться саме дії: перехід описали, а що при цьому має статися з грошима й листами — ні, тому на дії варто дивитися окремо.
Чим стан відрізняється від атрибута даних і чому це розрізнення критичне?
Станом варто вважати лише те, що змінює реакцію системи на подію; усе інше — атрибути, тобто тестові дані для інших технік. Замовлення на 100 грн і на 5000 грн — не два стани, а два значення поля: на подію «оплатити» система відповість однаково. Практичний тест на модель: «чи відповість система на цю подію інакше?» — якщо ні, це не окремий стан. Розрізнення критичне, бо якщо тягти в модель усі атрибути, машина розростеться до сотень «станів» і техніка помре під власною вагою. Це, до речі, типова помилка на практичних завданнях співбесід: кандидат будує «машину», де станами стали сума, місто доставки і спосіб оплати.
Чим техніка переходів станів відрізняється від таблиці рішень?
Таблиця рішень працює з комбінацією умов в один конкретний момент часу: усі входи є зараз, і результат визначається їхньою комбінацією. Переходи станів працюють з поведінкою, що залежить від історії: той самий запит від того самого користувача дає різний результат, бо система пройшла різний шлях до цього моменту. Тобто питання вибору техніки — «результат залежить від комбінації умов у моменті чи від послідовності подій у часі?». Якщо знижка рахується з типу клієнта і суми кошика — це таблиця рішень; якщо кнопка поводиться по-різному залежно від статусу замовлення — це переходи станів. Інтерв'юер таким питанням перевіряє, чи розумієш ти, коли яку техніку брати, а не завчене означення.
Навіщо будувати таблицю переходів, якщо вже є діаграма станів?
Діаграма показує лише ті переходи, які хтось не забув намалювати: чого немає на стрілках — того ніби й не існує, і це її сліпа пляма. Таблиця переходів (state table) закриває пляму механічно: рядки — стани, колонки — події, і кожна клітинка вимагає відповіді. Для замовлення з п'ятьма станами й чотирма подіями це 20 клітинок, з яких валідних переходів лише 5 — решта 15 прочерків і є головна цінність таблиці, бо кожен прочерк ставить питання «а що система зробить, якщо ця подія все-таки прилетить?». На практиці порядок такий: діаграму береш або малюєш для розуміння фічі й комунікації, таблицю будуєш для дизайну тестів і пошуку дірок. Сильна відповідь підкреслює: діаграма з документації показує happy path з голови аналітика, повноту дає лише таблиця.
Що таке невалідний перехід і як його знайти?
Невалідний перехід — комбінація стану і події, якої за моделлю бути не повинно: оплатити скасоване замовлення, скасувати доставлене, відправити неоплачене. Системно їх знаходять таблицею переходів: кожна порожня клітинка на перетині стану й події — кандидат у невалідні переходи. Важливо, що для невалідного переходу теж має бути визначена очікувана поведінка: «система ігнорує», «повертає помилку з поясненням», «показує актуальний стан» — це різні реакції, і яка правильна, мають сказати вимоги. Якщо вимоги мовчать — це знахідка ще до першого тесту. Якщо кандидат на співбесіді сам, без підказки, згадує невалідні переходи — для інтерв'юера це маркер зрілості.
Кнопки для дії немає в UI. Це означає, що перехід неможливий і тестувати нема чого?
Ні: «кнопки немає в UI» не означає «перехід неможливий», бо подія може прилетіти повз інтерфейс. Типові шляхи: прямий запит до API, друга вкладка браузера зі сторінкою, відкритою до зміни стану, кнопка «назад» на застарілу сторінку. Класичний сценарій: користувач відкрив замовлення у двох вкладках, в одній скасував, у другій натиснув «Оплатити» — сторінка ж стара, кнопка на ній ще є. Тому невалідні переходи перевіряють мінімум на двох рівнях: UI не пропонує дію, а бекенд відхиляє її, навіть якщо запит прийшов. Це два різні тести з різними багами: схована кнопка при дірявому API — досі відкритий шлях до оплати скасованого. Модель описує систему, а не один її інтерфейс.
Які рівні покриття існують у тестуванні переходів станів?
Три основні градації. «Усі стани» (all states) — кожен стан відвіданий хоч раз: найдешевше і найслабше, бо всі стани можна пройти, не перевіривши й половини переходів. «Усі валідні переходи» (valid transitions, він же 0-switch) — кожна стрілка діаграми пройдена хоч раз: практичний мінімум і найуживаніший критерій. «Усі переходи» (all transitions) — валідні плюс реакція на кожен невалідний: виправдано для критичних фіч на кшталт платіжного флоу, бо обсяг покриття — питання ризику. Покриття рахується як частка перевірених елементів від усіх елементів рівня. Пастка формулювання «я пройшов усі статуси»: звучить солідно, але в замовленні всі п'ять станів можна відвідати, пропустивши перехід «Оплачене → Скасоване» — а саме на ньому живе повернення коштів.
На співбесіді показують діаграму станів і питають: «Скільки тестів потрібно, щоб її покрити?» Як відповідати?
Це пастка без уточнення, і правильна перша реакція — спитати, який рівень покриття потрібен: усі стани, усі валідні переходи чи всі переходи включно з невалідними — числа будуть різні. Далі варто показати, що кількість тестів не дорівнює кількості переходів, бо один тест-ланцюжок покриває кілька стрілок поспіль: п'ять валідних переходів життєвого циклу замовлення закриваються трьома ланцюжками — повний щасливий шлях до доставлення, скасування нового і скасування оплаченого з перевіркою повернення коштів. Якщо потрібен рівень «усі переходи», до цього додаються перевірки невалідних комбінацій — по одній на кожен прочерк таблиці. Інтерв'юер дивиться на структуру міркування, а не на конкретне число.
Що таке n-switch coverage і які баги воно ловить?
Це глибші рівні покриття: перевірка послідовностей із двох і більше переходів поспіль (у літературі — 1-switch, n-switch coverage), тоді як звичайне покриття валідних переходів — 0-switch, кожна стрілка окремо. Такі рівні ловлять баги, які проявляються лише в певній послідовності: скасування працює, повторне замовлення працює, а «скасував і одразу замовив знову» — падає. Кожен перехід окремо пройдено й перевірено, але конкретна пара переходів поспіль лишає систему в кривому стані. На рівні джуна досить знати, що такі рівні існують і навіщо; на практиці їх вмикають точково для найризиковіших ділянок, бо кількість комбінацій росте швидко.
Наведи приклади машин станів у реальних продуктах.
Машини станів усюди, де у вимогах є «статус» чи «воркфлоу». Замовлення, платежі й доставка — канонічний приклад, у платіжних систем ще й статуси на боці провайдера. Підписки: тріал → активна → прострочена оплата → скасована, і переходи запускають листи, зміну доступів, повторні списання. Документообіг: чернетка → на погодженні → опубліковано → в архіві, де «відредагувати опубліковане» — типовий невалідний перехід. Воркфлоу в Jira — буквально конфігурована машина станів з дозволеними переходами між статусами. Є і дрібніші приклади: сесія жива → протухла, кнопка звичайна → у стані завантаження → заблокована — повторний клік у стані завантаження це подія в невалідному стані, і подвійне створення замовлення — її типовий наслідок. На співбесіді чекають конкретики з твого продукту: стани плюс хоча б один невалідний перехід, який ти реально перевіряв.
Як модель станів допомагає структурувати e2e-сюїту?
E2e-сценарій — це шлях через машину станів, і з цього випливає структура. Смоук — найкоротший щасливий шлях через цикл; повна регресія — набір шляхів, який закриває обраний рівень покриття. Коли покриття сформульоване в термінах переходів, легко аргументувати менеджеру, чому тестів саме стільки: не «я так відчуваю», а «ось стільки переходів, ось ланцюжки, які їх закривають». Стартовий стан для тесту конкретного переходу готується напряму — через API або сідинг даних, а не прокликуванням UI: тест переходу «Відправлене → Доставлене» не повинен щоразу проходити створення, оплату й відправлення, бо це три зайві точки відмови і зайвий час. І кожен тест мусить знати свій стартовий стан явно: тест, який неявно припускає стан, залишений попереднім тестом, — це перехід із невідомого стану, і його результат невідомий за означенням; саме тут корінь найпідступнішого флаку в стейтфул-фічах.
Чому один довгий e2e-тест через увесь життєвий цикл — антипатерн?
Бо це ланцюжок доміно: падіння на другому кроці ховає результати всіх наступних, і замість одного зрозумілого червоного тесту отримуєш «усе впало, розбирайся». Кожен ретест такого тесту починається з нуля й проходить усі попередні кроки заново, тож він ще й повільний. Правильний патерн: один тест — один перехід або короткий ланцюжок, стартовий стан підготовлений через API чи сідинг. Тоді падіння тесту одразу вказує на конкретний перехід, а не на «десь у життєвому циклі». Виняток, який лишається, — смоук як найкоротший щасливий шлях: там ланцюжок і є метою перевірки.
Як таблиця переходів працює як інструмент рев'ю вимог?
Таблиця змушує дати відповідь для кожної комбінації стану й події — і половина цих питань у вимогах відповіді не має. Що буде з коштами при скасуванні відправленого замовлення? Чи може адмін перевести доставлене назад у відправлене? Кожна порожня клітинка — це або свідоме рішення «ігноруємо», або дірка у вимогах, і з'ясувати різницю краще до розробки, ніж багом на проді. Окремо спливають дії переходів: перехід описано, а що при цьому відбувається з листами, грошима, аудит-логом — ні. Тому досвідчений QA будує таблицю ще на етапі аналізу вимог: щойно фіча описана як машина станів, питання до аналітика з неї випадають безкоштовно, і це найдешевші знахідки з усіх можливих.
Три кейси, як модель станів працює руками: таблиця переходів як рев'ю вимог документообігу (де питання до аналітика випадають ще до першого тесту), переклад таблиці в e2e-сюїту на Playwright зі стартовим станом через API і перевірка невалідного переходу через «застарілу» вкладку. Скрізь — що дивитися і чому.
Кейс 1. Таблиця переходів як рев'ю вимог
Вимоги до документообігу виглядають типово — три речення про щасливий шлях:
Чернетку можна редагувати й надіслати на погодження.
Погоджувач публікує документ або повертає його на доопрацювання.
Опублікований документ можна відправити в архів.
Виписуємо стани (Чернетка, На погодженні, Опубліковано, В архіві) та події (редагувати, надіслати на погодження, погодити, повернути, архівувати) і будуємо таблицю: 4 стани і 5 подій — 20 клітинок.
| Стан / Подія | редагувати | надіслати | погодити | повернути | архівувати |
|---|---|---|---|---|---|
| Чернетка | Чернетка | На погодженні | — | — | — |
| На погодженні | ? | — | Опубліковано | Чернетка | — |
| Опубліковано | ? | — | — | — | В архіві |
| В архіві | — | — | — | — | — |
Валідних переходів — 5. Але цінність не в них, а в знаках питання і прочерках, для яких вимоги мовчать:
- Редагування «на погодженні» — заборонити чи дозволити зі скиданням у чернетку? Це дві різні фічі з різними тестами, і вирішити мають вимоги, а не розробник за фактом.
- Редагування опублікованого — типовий невалідний перехід документообігу. Що очікуємо: помилку, тиху відмову чи створення нової версії? Кожен варіант — інший тест.
- З архіву немає жодної події. Це свідоме «архів назавжди» чи аналітик просто забув подію «розархівувати»? Дірка в моделі видима лише в таблиці — на діаграмі стан без вихідних стрілок виглядає нормально.
- Дії переходів не описані взагалі. Чи летить сповіщення авторові при поверненні на доопрацювання? Хто отримує лист при публікації? Перехід у вимогах є, дії — ні.
П'ять питань до аналітика до того, як написано перший тест, — найдешевші знахідки з усіх можливих.
Кейс 2. Від таблиці до e2e-сюїти: ланцюжки і стартовий стан через API
Життєвий цикл замовлення: 5 валідних переходів закриваються трьома ланцюжками — повний щасливий шлях (він же смоук), скасування нового і скасування оплаченого. Ключовий прийом: тест не прокликує UI до потрібного стану, а створює його через API.
import { test, expect } from '@playwright/test';
import { createOrderViaApi, getOrderViaApi, getRefundsViaApi } from './helpers/orders';
// ланцюжок 2: Нове → Скасоване (1 перехід)
test('нове замовлення переходить у скасоване', async ({ request }) => {
const order = await createOrderViaApi(request, { status: 'new' });
const res = await request.post(`/api/orders/${order.id}/cancel`);
expect(res.ok()).toBeTruthy();
const updated = await getOrderViaApi(request, order.id);
expect(updated.status).toBe('cancelled');
});
// ланцюжок 3: Нове → Оплачене → Скасоване — новий тут лише перехід зі стану paid,
// тому стартовий стан сідимо одразу оплаченим, без UI-оплати
test('скасування оплаченого запускає повернення коштів', async ({ request }) => {
const order = await createOrderViaApi(request, { status: 'paid' });
const res = await request.post(`/api/orders/${order.id}/cancel`);
expect(res.ok()).toBeTruthy();
// перевіряємо не лише стан, а й ДІЮ переходу — саме вона губиться найчастіше
const updated = await getOrderViaApi(request, order.id);
expect(updated.status).toBe('cancelled');
const refunds = await getRefundsViaApi(request, order.id);
expect(refunds).toHaveLength(1);
expect(refunds[0].amount).toBe(order.total);
});
Що дивитися і чому:
- Стартовий стан — аргумент
statusу сідингу, не ланцюжок кліків. Тест переходу «Оплачене → Скасоване» не мусить проходити створення й оплату через UI: це зайві точки відмови, які фарбують тест червоним через чужі баги. - Асерт на дію, не лише на стан.
status === 'cancelled'— половина перевірки; повернення коштів — дія переходу, і без асерту на refund тест пропустить найдорожчий баг цієї клітинки. - Три тести замість двадцяти і не одного. Рівень «усі валідні переходи» тут — три ланцюжки; аргументувати обсяг сюїти в термінах переходів простіше, ніж «здається, досить».
- Кожен тест створює власне замовлення. Тест, що підбирає замовлення, залишене попереднім тестом, — перехід із невідомого стану: результат невідомий за означенням, а флак гарантований.
Кейс 3. Невалідний перехід через застарілу вкладку
«Кнопки немає в UI» не рятує: друга вкладка зі сторінкою, відкритою до зміни стану, досі показує кнопку «Оплатити». Відтворюємо класику — скасував в одній вкладці, оплатив у другій.
test('оплата зі старої вкладки після скасування відхиляється', async ({ context, request }) => {
const order = await createOrderViaApi(request, { status: 'new' });
// дві вкладки дивляться на те саме нове замовлення
const tabA = await context.newPage();
const tabB = await context.newPage();
await tabA.goto(`/orders/${order.id}`);
await tabB.goto(`/orders/${order.id}`);
// у вкладці A скасовуємо — у базі стан уже cancelled
await tabA.getByRole('button', { name: 'Скасувати' }).click();
await expect(tabA.getByText('Скасоване')).toBeVisible();
// вкладка B застаріла: кнопка «Оплатити» на ній ще є — тиснемо
await tabB.getByRole('button', { name: 'Оплатити' }).click();
// очікувана реакція — з вимог: помилка з поясненням і актуальний стан
await expect(tabB.getByText('Замовлення вже скасоване')).toBeVisible();
// головний асерт: стан не змінився, гроші не списано
const updated = await getOrderViaApi(request, order.id);
expect(updated.status).toBe('cancelled');
});
Що дивитися і чому:
- Це другий тест, не заміна API-перевірки. Прямий
POST /payна скасованому замовленні має відхилятися окремим тестом (типово409 Conflict); вкладка перевіряє UI-шар — як застосунок поводиться, коли «неможлива» подія таки сталася. - Очікування «Замовлення вже скасоване» — з вимог, не з голови. «Ігнорує», «помилка», «показує актуальний стан» — різні контракти; якщо вимоги мовчать, це знахідка до написання тесту.
- Вирішальний асерт — через API, не через UI. Що б не намалювала вкладка B, істина в стані системи:
statusлишивсяcancelled, повторного списання немає. - Той самий патерн покриває кнопку «назад» і протухлу сесію. Будь-яка сторінка, відкрита до зміни стану, — джерело подій у невалідному стані; одна модель — ціла сім'я тестів.
Модель: стан, подія, перехід, дія
- Знаю чотири елементи моделі: стан (state), подія (event), перехід (transition), дія (action) — і можу пояснити кожен на прикладі замовлення.
- Можу відрізнити стан від атрибута даних тестом «чи відповість система на цю подію інакше?» — і пояснити, чому замовлення на 100 і на 5000 грн це не два стани.
- Знаю, що таке сторожова умова (guard condition) події: «оплатити» спрацьовує, лише якщо сума більша за нуль.
- Пам'ятаю, що підпис на стрілці діаграми читається «подія / дія», і що дії (лист, повернення коштів, аудит-лог) — те, що найчастіше губиться у вимогах.
Діаграма і таблиця переходів
- Розумію різницю: діаграма показує валідні переходи й годиться для комунікації, таблиця перебирає всі комбінації стану й події та робить невалідні переходи видимими.
- Знаю, що головна цінність таблиці — порожні клітинки (5 станів і 4 події — 20 клітинок, з них лише 5 валідних переходів): кожен прочерк — питання «що зробить система, якщо ця подія все-таки прилетить?».
- Розумію, що таблиця працює як інструмент рев'ю вимог: клітинка без відповіді — або свідоме «ігноруємо», або дірка у вимогах.
Невалідні переходи
- Можу пояснити, чому «кнопки немає в UI» не означає «перехід неможливий»: прямий запит до API, друга вкладка, сторінка, відкрита до зміни стану, кнопка «назад».
- Знаю, що невалідний перехід перевіряється на двох рівнях: UI не пропонує дію, а бекенд відхиляє запит, навіть якщо він прийшов.
- Розумію, що для невалідного переходу теж має бути визначена очікувана поведінка (ігнорувати, помилка з поясненням, показ актуального стану) — і що її диктують вимоги, а не звичка.
Рівні покриття
- Розрізняю три рівні: усі стани (all states), усі валідні переходи (0-switch), усі переходи — і знаю, який найслабший, а який практичний мінімум.
- Можу пояснити, чому «пройшов усі статуси» майже нічого не гарантує: всі стани можна відвідати, пропустивши перехід, на якому живе повернення коштів.
- Знаю, що кількість тестів не дорівнює кількості переходів: один тест-ланцюжок покриває кілька стрілок поспіль — 5 валідних переходів замовлення закриваються трьома ланцюжками.
- Знаю, що існують глибші рівні (1-switch, n-switch coverage) для послідовностей переходів і які баги «працює окремо — падає разом» вони ловлять.
- Розумію, що обсяг покриття невалідних переходів — питання ризику: платіжний флоу так, перемикач теми — навряд.
Машина станів у e2e
- Розумію, що e2e-сценарій — це шлях через машину станів: смоук — найкоротший щасливий шлях, регресія — набір шляхів під обраний рівень покриття.
- Знаю, чому стартовий стан тесту готують через API або сідинг даних, а не прокликуванням UI: менше точок відмови, менше часу.
- Можу пояснити, чому один довгий тест через увесь життєвий цикл — антипатерн: падіння на другому кроці ховає результати всіх наступних.
- Розумію, що тест без явного стартового стану — це перехід із невідомого стану з невідомим результатом, і саме тут корінь флаку в стейтфул-фічах.
- Пам'ятаю, що конкретний контракт відмови на невалідний перехід (
409,422, тихе ігнорування) визначають вимоги, і асертити його наосліп не можна.
Для якої фічі техніка переходів станів підходить найкраще?
Питання
Стан (state) у моделі переходів станів — що це?