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

    02 · Тест-дизайн

    Переходи станів

    Зміст

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

    Тестування переходів станів (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 валідних переходів закриваються трьома ланцюжками:

    1. Нове → Оплачене → Відправлене → Доставлене (3 переходи);
    2. Нове → Скасоване (1 перехід);
    3. Нове → Оплачене → Скасоване (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 — ізоляція тестів, без якої стартовий стан не гарантований.