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

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

    Класи еквівалентності

    Зміст

    Вичерпне тестування неможливе — це не гасло з підручника, а проста арифметика. Поле «вік» з допустимим діапазоном 18–65 приймає безліч можливих введень: усі цілі числа, дроби, відʼємні значення, букви, порожній рядок, пробіли, емодзі. Перебрати все — нереально; тицьнути навмання три значення — страшно, бо незрозуміло, що саме ти перевірив і що пропустив. Розбиття на класи еквівалентності (equivalence partitioning) — перша техніка тест-дизайну, яка перетворює цей хаос на короткий список тестів із зрозумілою логікою: чому саме ці значення і чому їх достатньо.

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

    Ідея розбиття

    Механізм такий: усі можливі значення ділимо на групи — класи еквівалентності — так, щоб за специфікацією система обробляла кожне значення всередині класу однаково. Тоді з кожного класу достатньо взяти одного представника. Якщо тест із представником пройшов — вважаємо, що пройшли б і решта значень класу; якщо впав — упав би весь клас. Саме ця гіпотеза еквівалентності й дає економію: замість тисяч значень — по одному на клас.

    Щоб розбиття було коректним, воно має відповідати двом правилам:

    • Класи не перетинаються — кожне значення належить рівно одному класу. Якщо значення 65 потрапляє і в «валідний діапазон», і в «занадто великі», межі класів визначені неохайно.
    • Класи покривають усе — будь-яке можливе введення має свій клас. Не тільки числа: рядок «abc», порожнє поле і пробіли теж кудись мають впасти.

    Для поля «вік 18–65» розбиття виглядає так:

    Усі можливі введення
    поля «вік»

    Невалідний клас:
    число менше 18

    Валідний клас:
    18–65

    Невалідний клас:
    число більше 65

    Невалідний клас:
    не число (abc, порожньо, пробіли)

    Представник: 10

    Представник: 30

    Представник: 80

    Представник: abc

    Усі можливі введення
    поля «вік»

    Невалідний клас:
    число менше 18

    Валідний клас:
    18–65

    Невалідний клас:
    число більше 65

    Невалідний клас:
    не число (abc, порожньо, пробіли)

    Представник: 10

    Представник: 30

    Представник: 80

    Представник: abc

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

    Важливе чесне застереження: «обробляються однаково» — це гіпотеза за специфікацією, а не факт про код. Реалізація може ділити діапазон інакше, ніж написано у вимогах, і найчастіше розбіжності живуть на межах класів — там, де 17 стає 18. Тому класи еквівалентності майже ніколи не застосовують окремо: їхній природний напарник — аналіз граничних значень (boundary value analysis), який перевіряє саме краї кожного класу.

    Валідні й невалідні класи

    Класи ділять на дві принципово різні групи:

    • Валідні — значення, які система має прийняти й обробити за основним сценарієм. Для віку це діапазон 18–65.
    • Невалідні — значення, які система має відхилити: показати помилку, не пропустити далі, не зламатися. Для віку — числа поза діапазоном і все, що числом не є.

    Новачки зазвичай зосереджуються на валідних класах: перевірив, що 30 приймається, — і поле «протестоване». На практиці дуже багато дефектів живе саме в обробці невалідного: забутий валідатор, крива помилка, 500-ка замість акуратного повідомлення, «abc» яке пролізло в базу. Валідний клас перевіряє, що фіча працює; невалідні — що вона не розвалюється від реального користувача, який вставить у поле що завгодно.

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

    Класи входу і виходу

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

    Найкорисніші на практиці після входів — класи виходу. Приклад: інтернет-магазин рахує знижку — 0% для замовлень до 1000 грн, 5% від 1000 до 5000, 10% понад 5000. На вході — сума замовлення, але подивись на вихід: система може видати рівно три різні знижки. Це три вихідні класи, і кожен вартий тесту. Щоб покрити вихідний клас, підбираєш вхід, який його породжує: замовлення на 500 грн → 0%, на 3000 → 5%, на 7000 → 10%.

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

    Кожен невалідний клас — окремим тестом

    Правило, яке часто перевіряють на співбесідах: валідні значення можна комбінувати в одному тесті, а невалідні — ні, кожне йде окремим кейсом, поки решта полів валідні.

    Причина називається маскуванням дефектів (fault masking). Уяви форму реєстрації з полями «вік» та «email». Ти вирішив зекономити і в одному тесті ввів вік «abc» і email без «@». Форма показала помилку про вік — тест «пройшов». Але валідацію email ти не перевірив: система могла зупинитися на першій помилці й до другої просто не дійти. Якщо валідатор email зламаний — ти про це не дізнаєшся, поки не дізнається користувач.

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

    Комбінування без вибуху кейсів

    Поки поле одне — все просто. Але форма з кількох полів провокує перемножувати: кожен клас першого поля з кожним класом другого і третього. Порахуймо на формі замовлення:

    • вік: 1 валідний клас + 3 невалідні (менше 18, більше 65, не число);
    • email: 1 валідний + 2 невалідні (без «@», порожній);
    • доставка: 2 валідні (курʼєр і самовивіз — різні гілки логіки) + 1 невалідний (не вибрано).

    Повне перемноження: 4 × 3 × 3 = 36 кейсів на три нещасні поля. Додай четверте — і рахунок піде на сотні. Це і є комбінаторний вибух.

    Вихід — покриття «кожен вибір» (each choice coverage): кожен клас має зʼявитися хоча б в одному тесті, але не в усіх можливих поєднаннях. Рецепт:

    1. Валідні класи пакуємо разом. Один тест може закрити по валідному класу з кожного поля одночасно. Тестів треба стільки, скільки валідних класів у «найбагатшого» поля — тут два (курʼєр і самовивіз).
    2. Невалідні — по одному за раз, решта полів валідні. Тестів — сума всіх невалідних класів: 3 + 2 + 1 = 6.

    Разом 2 + 6 = 8 тестів замість 36:

    #ВікEmailДоставкаЩо закриває
    130user@mail.comкурʼєрусі валідні класи + гілка «курʼєр»
    242olia@mail.comсамовивізгілка «самовивіз»
    312user@mail.comкурʼєрвік менше 18
    480user@mail.comкурʼєрвік більше 65
    5abcuser@mail.comкурʼєрвік не число
    630без «@»курʼєрневалідний email
    730порожнійкурʼєрпорожній email
    830user@mail.comне вибранодоставка не вибрана

    Чесна межа техніки: each choice достатньо, коли поля незалежні — поведінка кожного не залежить від значень сусідів. Якщо логіка спрацьовує саме на поєднаннях («знижка діє лише для курʼєра І суми понад 1000»), потрібні таблиці рішень (decision tables), а для великих конфігураційних матриць — попарне тестування (pairwise testing).

    Класи як параметризований автотест

    Потребує: JS/TS. Блок можна пропустити при першому проході — суть глави від цього не постраждає.

    Класи еквівалентності природно лягають на параметризовані тести: техніка дає таблицю «значення → очікування», а тест лишається один — цикл по рядках таблиці. У Playwright це виглядає так:

    import { test, expect } from '@playwright/test';
    
    // Кожен рядок — окремий невалідний клас зі своїм очікуваним повідомленням
    const invalidAges = [
      { value: '17', error: 'Мінімальний вік — 18 років', title: 'нижче мінімуму' },
      { value: '66', error: 'Максимальний вік — 65 років', title: 'вище максимуму' },
      { value: 'abc', error: 'Вік має бути числом', title: 'не число' },
      { value: '', error: 'Поле обовʼязкове', title: 'порожнє значення' },
    ];
    
    for (const { value, error, title } of invalidAges) {
      test(`реєстрація відхиляє вік: ${title}`, async ({ page }) => {
        await page.goto('/registration');
        await page.getByLabel('Вік').fill(value);
        await page.getByLabel('Email').fill('user@mail.com'); // решта полів — валідні
        await page.getByRole('button', { name: 'Зареєструватися' }).click();
        await expect(page.getByText(error)).toBeVisible();
      });
    }

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

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

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

    • Кілька значень з одного класу. Тести з віком 25, 30 і 40 виглядають як ретельність, а насправді це один клас: три тести, які завжди падатимуть і проходитимуть разом. Два з них — баласт, який доведеться підтримувати.
    • «Невалідний ввід» одним класом. Виглядає як один акуратний негативний тест, а насправді порожнє поле, букви і число поза діапазоном проходять різними валідаторами й дають різні повідомлення — це різні класи, і зламатися кожен може окремо.
    • Розбиття без «сміттєвих» класів. Три класи «менше / діапазон / більше» виглядають як повне покриття, а насправді нечислові введення нікуди не впали — правило «кожне значення має свій клас» порушено. Перевірка повноти проста: назви будь-яке дике введення і спитай себе, в який клас воно потрапляє.
    • Кілька невалідних значень в одному тесті. Виглядає як економія кейсів, а насправді маскування дефектів: система зупиняється на першій помилці, і решта валідаторів лишаються неперевіреними.
    • Представник із середини як єдина перевірка класу. Клас виглядає однорідним за специфікацією, а насправді реалізація може «відрізати» його межі неправильно — класика off-by-one. Тому представник із середини доповнюють перевірками країв — це вже граничні значення.

    Підсумок

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

    Що питають на співбесіді

    • «Поле приймає числа від 1 до 100. Скільки тестів потрібно і чому?» Інтервʼюер дивиться, чи назвеш ти невалідні класи (менше 1, більше 100, не число, порожньо), а не лише «перевірю 50». І чи поясниш, чому 50 і 70 — це один тест, а не два. Повна сильна відповідь включає й межі — це вже звʼязка з граничними значеннями.
    • «Чим класи еквівалентності відрізняються від граничних значень?» Очікувана суть: класи відповідають на питання «які групи значень існують», межі — «де саме групи стикаються»; техніки застосовують разом, а не замість.
    • «Чому не можна перевірити два невалідні поля одним тестом?» Ключове слово — маскування: система зупиняється на першій помилці, друга валідація лишається неперевіреною.
    • «Форма з пʼяти полів — як не отримати сотні кейсів?» Тут чекають на each choice: валідні пакуємо, невалідні по одному. Якщо згадаєш, що для залежних полів це не працює і потрібна таблиця рішень, — плюс до senior-балів.
    • «Протестуйте поле вводу». Класика, в якій класи еквівалентності — скелет відповіді: спершу уточнити вимоги, потім валідні й невалідні класи, потім межі. Повний розбір таких задач — у главі про класичні задачі співбесід.

    Загалом інтервʼюер оцінює не заучене означення, а системність: чи є в твоїй відповіді словник (валідний/невалідний клас, представник, покриття), чи вмієш ти порахувати й обґрунтувати кількість тестів, і чи бачиш межі техніки.

    Джерела

    • ISTQB CTFL Syllabus v4.0 — розділ 4.2.1 «Equivalence Partitioning»: означення, валідні/невалідні класи, покриття та each choice coverage. Правило «кожен невалідний клас — окремо» явно сформульоване в попередній версії силабуса (CTFL 2018 v3.1, розділ 4.2.1).
    • ISTQB Glossary — канонічні означення термінів equivalence partitioning та coverage item.
    • Playwright: Parameterize tests — офіційна документація патерну «цикл по тестових даних» із прикладу вище.