Граничні значення
Зміст
Якщо баг десь і живе, то найімовірніше — на межі. Поле приймає від 1 до 100, і саме на 0, 1, 100 та 101 зʼясовується, що розробник переплутав «більше» з «більше або дорівнює». Аналіз граничних значень (boundary value analysis, BVA) — техніка, яка цілиться саме туди: не в середину діапазону, де все зазвичай працює, а в точки, де поведінка системи змінюється.
На співбесідах це одна з найчастіших тем: питання «скільки тестів потрібно для поля 1–100» — класика, за якою інтервʼюер за дві хвилини бачить, чи мислиш ти системно, чи перелічуєш значення навмання. Ця глава — про механізм: чому межі притягують дефекти, як від класів еквівалентності перейти до граничних точок і як порахувати кейси так, щоб відповідь можна було захистити.
Чому баги скупчуються на межах
Будь-який діапазон у вимогах перетворюється в коді на порівняння: >=, <, +1 до лічильника, обрізання рядка до N символів. Кожне таке порівняння — місце, де легко помилитися рівно на одиницю. Цей клас дефектів так і називається — помилка на одиницю (off-by-one error).
Звідки вона береться:
- Переплутані оператори. Вимога каже «знижка від 1000 грн включно», а в коді:
function hasDiscount(total: number): boolean {
return total > 1000; // off-by-one: втрачено «включно», треба >=
}
Для 999 і 1500 функція працює правильно. Ламається вона рівно в одній точці — 1000. Тест із «типовим» значенням цього ніколи не побачить.
- Неоднозначні вимоги. «Термін дії — до 31 грудня»: 31 грудня ще діє чи вже ні? «Доступно з 18 років»: у день 18-річчя — можна? Аналітик мав на увазі одне, розробник прочитав інакше — і межа зсунулася ще до написання коду.
- Проблема стовпів і прольотів (fencepost error). Паркан завдовжки 10 метрів із секціями по метру потребує не 10 стовпів, а 11. Той самий ефект у коді: цикл «від 1 до N» проти «від 0 до N−1», пагінація, індекси масивів — рахуємо елементи, а треба межі між ними.
Механізм простий: усередині класу еквівалентності всі значення — за припущенням техніки — проходять тією самою гілкою коду, тому 50 і 73 для системи нерозрізненні. А 100 і 101 — по різні боки умови. Саме тому дефекти «розмазані» не рівномірно по діапазону, а сконцентровані там, де умова перемикається.
Воркфлоу: від класів еквівалентності до меж
BVA — не самостійна техніка з нуля, а надбудова над розбиттям на класи еквівалентності. Спершу ділиш вхід на класи, потім дивишся на точки, де класи стикаються. Важливе обмеження: межі існують лише там, де значення впорядковані — числа, довжини, дати, розміри. У класу «спосіб оплати: картка / готівка» меж немає, там працюють інші техніки (наприклад, таблиці рішень).
Крок «уточнити межу» — не формальність. Якщо у вимозі написано «до 100», а ти мовчки вирішив, що 100 валідне, — ти протестував свою гіпотезу, а не вимогу. Питання «включно чи ні» ставиться аналітику до дизайну тестів, і саме воно нерідко знаходить дефект ще у вимозі — найдешевший баг із можливих.
2- і 3-точковий аналіз
Скільки значень перевіряти навколо кожної межі — питання, на яке ISTQB дає дві відповіді.
2-точковий (2-value BVA): для кожної межі береться саме граничне значення і найближче значення по інший бік — у сусідньому класі. Для нижньої межі діапазону 1–100 це 1 і 0, для верхньої — 100 і 101.
3-точковий (3-value BVA): граничне значення та обидва його сусіди, незалежно від того, в якому класі вони лежать. Для тих самих меж: 0, 1, 2 і 99, 100, 101.
Навіщо третя точка, якщо дві вже покривають обидва боки? Вона ловить дефекти, у яких межа реалізована не зсувом, а точковим влученням. Повернімося до знижки «від 1000 включно» і уявімо реалізацію:
return total === 1000; // мало бути total >= 1000
2-точковий аналіз перевіряє 1000 (знижка є — правильно) і 999 (знижки немає — теж правильно). Обидва тести зелені, баг живий. І лише значення 1001 — третя точка — показує, що для сум понад 1000 знижка зникла. Звучить як екзотика, але подібні дефекти виникають цілком буденно: наприклад, коли умову рефакторили з ланцюжка if/else і одна з гілок загубилася.
Практичне правило: 2-точковий — розумний дефолт; 3-точковий — для меж із високим ризиком (гроші, доступи, ліміти безпеки) або коли реалізація історично багата на сюрпризи. Три точки замість двох — це в півтора раза більше кейсів на кожну межу, і ця ціна має бути чимось виправдана.
Класика: скільки тестів для поля 1–100
Тепер саме питання співбесіди. Поле приймає цілі числа від 1 до 100 включно. Рахуємо вголос.
Класи еквівалентності: невалідний «менше 1», валідний «1–100», невалідний «понад 100». Межі — точки стику: 1 і 100.
| Підхід | Значення | Кейсів |
|---|---|---|
| Лише класи еквівалентності | −5, 50, 200 (по одному з класу) | 3 |
| 2-точковий BVA | 0, 1, 100, 101 | 4 |
| 3-точковий BVA | 0, 1, 2, 99, 100, 101 | 6 |
Зверни увагу: 2-точковий BVA робить окремий тест «типового значення» необовʼязковим — значення 1 і 100 самі лежать у валідному класі й покривають його. Додати всередину щось на кшталт 50 не заборонено, але це вже перестраховка, а не вимога техніки.
Сильна відповідь на співбесіді — не число, а міркування перед ним: «Уточню: цілі числа? Обидві межі включно? Тоді класів три, меж дві; 2-точковий аналіз дає 4 тести — 0, 1, 100, 101; якщо межа критична, розширюю до 3-точкового — 6 тестів». Слабка відповідь — випалити «шість!» і не пояснити, звідки вони взялися. А повний розбір «протестуйте поле вводу» з усіма невалідними типами входу — у главі Класичні задачі співбесід.
Межі є не лише в числах
Техніка та сама, змінюється лише «лінійка», уздовж якої впорядковані значення.
| Тип даних | Типові межі |
|---|---|
| Числа | 0, −1, мінімум/максимум діапазону, максимум ± 1, переповнення типу |
| Рядки | порожній рядок, 1 символ, максимальна довжина, максимум + 1 |
| Колекції | порожній список, 1 елемент, рівно розмір сторінки, розмір + 1, максимум |
| Дати | кінець місяця (28/29/30/31), 29 лютого, 31 грудня → 1 січня, переведення годинника |
| Файли | 0 байтів, рівно ліміт розміру, ліміт + 1 |
Кілька неочевидних пасток у цій таблиці.
Рядки: довжина в чому? Ліміт «255 символів» може рахуватися в символах, байтах чи кодових одиницях — і це різні числа. У JavaScript '😀'.length дорівнює 2, бо емодзі кодується сурогатною парою. Поле, що «приймає 255 символів», може відхилити 128 емодзі — і це або баг, або недомовлена вимога. Межа + юнікод — подвійний удар, який вартий окремого кейсу.
Колекції: розмір сторінки — це теж межа. Список на 20 елементів при пагінації по 20: рівно 20 (одна сторінка без другої), 21 (зʼявляється друга сторінка з одним елементом), 0 (порожній стан). Баги «зникла остання позиція» і «порожня друга сторінка» — класичні off-by-one у пагінації.
Дати: межі багатовимірні. 29 лютого 2024-го існує, а 29 лютого 2100-го — ні (рік ділиться на 100, але не на 400). Перехід через північ ламає логіку «сьогодні/вчора», перехід на літній час робить одну добу на годину коротшою. Окрема історична межа — 19 січня 2038 року, коли переповниться 32-бітний знаковий лічильник секунд Unix-часу: у legacy-системах ця дата — цілком реальне граничне значення для полів «термін дії».
Числа: межі типу, а не лише вимоги. Максимум 32-бітного знакового цілого — 2 147 483 647; у JavaScript безпечна цілочисельна межа — Number.MAX_SAFE_INTEGER (9 007 199 254 740 991), далі арифметика починає втрачати точність. Вимога може мовчати про ці числа, але код на них реагує.
Відкриті інтервали і неявні межі
Дві речі, які відрізняють механічне застосування техніки від осмисленого.
Відкриті й закриті інтервали. Закритий інтервал [1; 100] включає обидві межі, відкритий (1; 100) — жодної, напіввідкритий [1; 100) — лише ліву. Людською мовою вимог ці відтінки звучать як «від», «до», «не більше», «старше» — і кожне з них хтось колись прочитає навпаки. Для тестувальника відкритість межі змінює розклад значень: якщо 100 не входить у діапазон, то валідна межа — 99, а 100 — уже перший невалідний сусід. Тому перше питання до будь-якої межі: «включно?»
Неявні межі. У вимогах їх немає, а в системі — є:
- довжина колонки в базі: рядок обрізається або запит падає на 256-му символі, хоча форма «безрозмірна»;
- ліміт тіла запиту чи розміру файлу на боці сервера або проксі;
- переповнення числового типу, до якого вимога «введіть кількість» просто не дійшла думкою;
- ліміти сторонніх API — довжина повідомлення, кількість елементів у батчі (формати й контракти — у главі REST API та формати даних).
Евристика для пошуку: круглі для компʼютера числа — 255, 256, 1024, 65 535 — і технологічні дефолти стека. Якщо вимога не називає максимум, це не означає, що його немає; це означає, що його ніхто не перевіряв.
Граничні значення в автотесті
BVA природно лягає на параметризовані тести: значення й очікування — у таблицю, логіка — одна.
const cases = [
{ qty: 0, valid: false }, // нижня межа − 1
{ qty: 1, valid: true }, // нижня межа
{ qty: 100, valid: true }, // верхня межа
{ qty: 101, valid: false }, // верхня межа + 1
];
for (const { qty, valid } of cases) {
test(`кількість ${qty}: ${valid ? 'приймається' : 'відхиляється'}`, async ({ page }) => {
await page.getByLabel('Кількість').fill(String(qty));
await page.getByRole('button', { name: 'Додати' }).click();
const error = page.getByText('Кількість має бути від 1 до 100');
if (valid) {
await expect(error).toBeHidden();
} else {
await expect(error).toBeVisible();
}
});
}
Кожне граничне значення — окремий кейс з окремим вердиктом у звіті: одразу видно, який саме бік межі зламався. Про те, як такі набори значень генерувати й ізолювати між прогонами, — глава Тестові дані.
Типові помилки
- Виглядає як покриті межі — «перевірив 1 і 100», — а насправді перевірено лише валідний бік. Off-by-one на кшталт
> 1замість>= 1ці тести не зловлять: без 0 і 101 техніка застосована наполовину. - Виглядає як економія — обʼєднати 0 і 101 в один прогін форми, — а насправді маскування дефектів: якщо валідація зупиняється на першій помилці, друга межа залишається неперевіреною. Кожне невалідне граничне значення — окремий кейс, той самий принцип, що й у класах еквівалентності.
- Виглядає як педантичність — питати «включно чи ні», — а насправді це і є тест вимоги. Мовчки припустивши відповідь, ти ризикуєш зеленими тестами навколо зсунутої межі: перевірено не те, що мав на увазі бізнес.
- Виглядає як ґрунтовність — 3-точковий аналіз на кожне поле, — а насправді роздування сюїти без пропорційного виграшу. Третя точка виправдана ризиком конкретної межі, а не звичкою.
- Виглядає як «межі — це про числа», а насправді найболючіші межі живуть у датах, довжинах рядків і розмірах сторінок — там, де вимога навіть не згадує чисел.
Підсумок
- Дефекти концентруються там, де код змінює поведінку: кожне порівняння — кандидат на off-by-one, тому межі тестуються завжди, а середина класу — за залишковим принципом.
- BVA — надбудова над класами еквівалентності й працює лише для впорядкованих значень: спершу класи, потім точки їхнього стику.
- 2-точковий аналіз: межа + сусід у суміжному класі; 3-точковий: межа + обидва сусіди. Другий ловить точкові дефекти реалізації ціною більшої кількості кейсів.
- Для поля 1–100: 4 тести у 2-точковому (0, 1, 100, 101), 6 — у 3-точковому (0, 1, 2, 99, 100, 101). Відповідь без ходу міркувань не зараховується.
- Межа буває неявною: ліміти БД, типів даних і сторонніх API існують незалежно від того, чи згадала їх вимога.
Що питають на співбесіді
- «Поле приймає від 1 до 100. Скільки тестів і які?» — питання-фільтр. Інтервʼюер дивиться не на число, а на порядок дій: чи уточниш тип даних і включність меж, чи назвеш класи, чи поясниш різницю між 4 і 6 кейсами. Відповідь «перевірити всі сто значень» — миттєвий мінус.
- «Чим граничні значення відрізняються від класів еквівалентності?» — очікують розуміння, що це не конкурентні техніки, а послідовні кроки: класи дають структуру, межі — найризикованіші точки всередині неї.
- «Що таке off-by-one і де ти його ловив?» — перевірка, чи бачиш ти звʼязок техніки з реальними багами. Сильна відповідь — конкретний приклад: пагінація, умова знижки, обрізання рядка.
- «Назви граничні значення для поля дати народження» — тест на широту мислення: 29 лютого, сьогоднішня дата (народитися «завтра» не можна), мінімальний реалістичний рік, вік рівно 18. Хто називає лише «мінімум і максимум», той вивчив визначення, але не техніку.
- «Коли 3-точковий аналіз замість 2-точкового?» — питання на рівень вище: очікують аргумент через ризик і тип дефектів, а не «бо так надійніше».
Джерела
- ISTQB CTFL 4.0 Syllabus — розділ 4.2.2 «Boundary Value Analysis»: канонічні визначення 2- і 3-точкового підходів, які питають на сертифікації та співбесідах.
- ISTQB Glossary — офіційні означення boundary value analysis, boundary value та equivalence partitioning.
- MDN: Number.MAX_SAFE_INTEGER — межі безпечної цілочисельної арифметики в JavaScript.
- Playwright: Parameterize tests — офіційний патерн параметризації, яким граничні значення перетворюються на табличні автотести.
Що таке аналіз граничних значень і навіщо він потрібен?
Аналіз граничних значень (boundary value analysis, BVA) — техніка тест-дизайну, яка перевіряє точки, де поведінка системи змінюється: межі діапазонів, лімітів, довжин. Механізм простий: будь-який діапазон у вимогах перетворюється в коді на порівняння — >=, <, обрізання до N символів — і кожне таке порівняння є місцем, де легко помилитися рівно на одиницю. Усередині діапазону всі значення проходять тією самою гілкою коду, тому 50 і 73 для системи нерозрізненні, а от 100 і 101 лежать по різні боки умови. Практичний наслідок: тести цілять у межі, а не в середину, бо дефекти сконцентровані саме там, де умова перемикається.
Чому дефекти концентруються саме на межах діапазонів?
Тому що межа з вимоги стає порівнянням у коді, а порівняння — класичне джерело помилки на одиницю (off-by-one error). Типові причини три: переплутані оператори (> замість >= — «знижка від 1000 включно» не спрацьовує рівно на 1000), неоднозначні вимоги («до 31 грудня» — включно чи ні? аналітик мав на увазі одне, розробник прочитав інакше) і проблема стовпів та прольотів (fencepost error): паркан 10 метрів із секціями по метру потребує 11 стовпів, а не 10 — той самий ефект у циклах, пагінації, індексах масивів. У результаті функція працює для 999 і 1500, а ламається в одній точці — 1000, і тест із «типовим» значенням цього ніколи не побачить.
Чим граничні значення відрізняються від класів еквівалентності?
Це не конкурентні техніки, а послідовні кроки: BVA — надбудова над розбиттям на класи еквівалентності. Спершу вхід ділиться на класи, потім береться точка, де класи стикаються, — це і є межа. Класи дають структуру й гарантують, що кожна гілка поведінки взагалі покрита; межі додають найризикованіші точки всередині цієї структури. Важливе обмеження: межі існують лише там, де значення впорядковані — числа, довжини, дати, розміри; у класу «спосіб оплати: картка / готівка» меж немає, і BVA там незастосовний.
Що таке off-by-one і де ти його ловив?
Off-by-one — дефект, коли логіка помиляється рівно на одиницю: умова спрацьовує на значення раніше чи пізніше, ніж мала, або цикл робить на одну ітерацію більше чи менше. Народжується він у порівняннях (> замість >=), у рахунку «елементів проти меж між ними» (fencepost) і в неоднозначних вимогах, де межа зсунулася ще до написання коду. Сильна відповідь тут — конкретний приклад: пагінація, де при рівно 20 елементах зникає остання позиція або зʼявляється порожня друга сторінка; умова знижки «від 1000», що не спрацьовує рівно на 1000; рядок, який обрізається на 256-му символі. Саме тому в кейсах перевіряються значення точно на межі та її сусіди — вони й ловлять цей клас багів.
Поле приймає цілі числа від 1 до 100. Скільки тестів потрібно і які саме?
Сильна відповідь починається з уточнень: цілі числа? обидві межі включно? Далі — хід міркувань: класів еквівалентності три (менше 1 — невалідний, 1–100 — валідний, понад 100 — невалідний), меж дві — 1 і 100. Двоточковий аналіз дає 4 тести: 0, 1, 100, 101 — межа плюс найближчий сусід у суміжному класі. Якщо межа критична (гроші, доступи), розширюю до 3-точкового — 6 тестів: 0, 1, 2, 99, 100, 101. Просто «лише класи еквівалентності» дали б 3 тести (по одному з класу), але не зловили б off-by-one. Випалити число без пояснення — слабка відповідь: інтервʼюер дивиться не на цифру, а на порядок дій.
У чому різниця між 2-точковим і 3-точковим граничним аналізом?
Двоточковий (2-value BVA) бере для кожної межі саме граничне значення і найближче значення по інший бік — у сусідньому класі: для нижньої межі діапазону 1–100 це 1 і 0. Триточковий (3-value BVA) бере межу та обох її сусідів незалежно від класу: 0, 1, 2. Обидва варіанти канонічні за ISTQB, тому на співбесіді коректно назвати обидва й пояснити, коли який. Практична різниця — у кількості кейсів (для поля 1–100: 4 проти 6) і в типі дефектів, які ловляться: третя точка потрібна для точкових помилок реалізації, які 2-точковий пропускає.
Навіщо третя точка, якщо 2-точковий уже перевіряє обидва боки межі?
Третя точка ловить дефекти, у яких межа реалізована не зсувом, а точковим влученням. Класичний приклад: замість total >= 1000 розробник (наприклад, після невдалого рефакторингу ланцюжка if/else) лишив total === 1000. Двоточковий аналіз перевіряє 1000 — знижка є, правильно — і 999 — знижки немає, теж правильно: обидва тести зелені, баг живий. І лише 1001 — третя точка — показує, що для сум понад 1000 знижка зникла. Практичне правило: 2-точковий — розумний дефолт, 3-точковий — для меж із високим ризиком (гроші, доступи, ліміти безпеки), бо три точки замість двох — це в півтора раза більше кейсів на кожну межу, і ця ціна має бути виправдана.
Чи потрібен окремий тест «типового значення» всередині діапазону, якщо межі вже покриті?
За технікою — ні: у 2-точковому BVA значення 1 і 100 самі лежать у валідному класі й покривають його, тож окремий тест на 50 стає необовʼязковим. Додати «типове» значення не заборонено, але це вже перестраховка, а не вимога техніки — усередині класу всі значення проходять тією самою гілкою коду, і 50 не перевірить нічого, чого не перевірили 1 і 100. На співбесіді це гарний маркер розуміння: кандидат, який рахує «0, 1, 50, 100, 101», механічно змішує дві техніки, а той, хто пояснює, чому 50 зайве, показує, що розуміє механізм еквівалентності.
У вимозі написано «термін дії — до 31 грудня». Які тести спроєктуєш?
Перший крок — не проєктувати тести, а поставити питання аналітику: 31 грудня ще діє чи вже ні? «До», «від», «не більше», «старше» — формулювання, які хтось колись прочитає навпаки, і межа зсувається ще до написання коду. Це відповідає різниці між закритим інтервалом [a; b] (обидві межі входять), відкритим (a; b) (жодна) і напіввідкритим [a; b) (лише ліва). Від відповіді залежить розклад значень: якщо 31 грудня включно, то валідна межа — 31.12, а перший невалідний сусід — 01.01; якщо ні — валідна межа зсувається на 30.12. Питання «включно чи ні» — не педантичність, а тест вимоги: воно нерідко знаходить дефект ще до коду, і це найдешевший баг із можливих.
Назви граничні значення для текстового поля з лімітом 255 символів.
База: порожній рядок, 1 символ, рівно 255, 256 — обидві межі довжини та перший сусід за кожною. Але сильніша частина відповіді — питання «довжина в чому?»: ліміт може рахуватися в символах, байтах чи кодових одиницях, і це різні числа. У JavaScript '😀'.length дорівнює 2, бо емодзі кодується сурогатною парою, — тож поле, що «приймає 255 символів», може відхилити 128 емодзі, і це або баг, або недомовлена вимога. Окремо варто перевірити неявну межу: навіть якщо форма «безрозмірна», колонка в базі має довжину, і рядок або обріжеться, або запит упаде на 256-му символі. Межа плюс юнікод — подвійний удар, який вартий окремого кейсу.
Список показується з пагінацією по 20 елементів. Які граничні значення тут є?
Розмір сторінки — це теж межа, хоча у вимогах вона часто не звучить як діапазон. Ключові значення: 0 елементів (порожній стан), 1 елемент, рівно 20 (одна сторінка — другої бути не повинно), 21 (зʼявляється друга сторінка з одним елементом). Саме тут живуть класичні off-by-one пагінації: «зникла остання позиція» і «порожня друга сторінка» — наслідки плутанини між «до N» і «до N включно» або рахунку елементів замість меж між ними (fencepost). Додатково є верхня межа колекції — максимум, який система взагалі готова віддати. Для автотестів кожне з цих значень — окремий кейс з окремим вердиктом, щоб у звіті було видно, який саме бік межі зламався.
Назви граничні значення для поля дати народження.
Це питання на широту мислення: дати — багатовимірна лінійка, і «мінімум/максимум» — лише початок. Календарні межі: кінець місяця (28/29/30/31), 29 лютого — воно існує у 2024-му, але не у 2100-му (рік ділиться на 100, але не на 400). Логічні межі домену: сьогоднішня дата (народитися «завтра» не можна), вік рівно 18 там, де є перевірка повноліття, мінімальний реалістичний рік. Технічні межі: перехід через північ ламає «сьогодні/вчора», переведення годинника робить добу коротшою на годину, а в legacy-системах є 19 січня 2038 року — переповнення 32-бітного знакового лічильника секунд Unix-часу. Хто називає лише «мінімум і максимум», той вивчив визначення, але не техніку.
Що таке неявні межі і як їх шукати, якщо у вимогах їх немає?
Неявні межі — ліміти, яких немає у вимогах, але які існують у системі: довжина колонки в базі (рядок обрізається чи запит падає на 256-му символі, хоча форма «безрозмірна»), ліміт тіла запиту на сервері чи проксі, переповнення числового типу, ліміти сторонніх API на довжину повідомлення чи розмір батча. Окремий шар — межі самих типів: максимум 32-бітного знакового цілого — 2 147 483 647, а в JavaScript безпечна цілочисельна межа — Number.MAX_SAFE_INTEGER, далі арифметика втрачає точність. Евристика пошуку: круглі для компʼютера числа — 255, 256, 1024, 65 535 — і технологічні дефолти стека. Ключова думка: якщо вимога не називає максимум, це не означає, що його немає, — це означає, що його ніхто не перевіряв.
Як оформити граничні значення в автотестах і чому не можна обʼєднувати кілька невалідних меж в один кейс?
BVA природно лягає на параметризовані тести: значення й очікування виносяться в таблицю кейсів, логіка перевірки одна, а кожне граничне значення стає окремим тестом з окремим вердиктом у звіті — одразу видно, який бік межі зламався. Обʼєднувати 0 і 101 в один прогін форми виглядає як економія, а насправді маскує дефекти: якщо валідація зупиняється на першій помилці, друга межа залишається неперевіреною. Кожне невалідне граничне значення — окремий кейс, той самий принцип, що й у класах еквівалентності. Побічний бонус параметризації — дешево розширити 2-точковий набір до 3-точкового для критичної межі: додаються два рядки в таблицю, а не два нові тести з дубльованою логікою.
Три кейси, де граничні значення відпрацьовують на повну: розбір вимоги «знижка від 1000 грн» від уточнення до таблиці кейсів, off-by-one у пагінації, який ловиться рівно на розмірі сторінки, і поле з лімітом 255 символів, де межа стикається з юнікодом. Скрізь — хід міркувань і що дивитися.
Кейс 1. «Знижка від 1000 грн»: від вимоги до таблиці кейсів
Вимога: «безкоштовна доставка для замовлень від 1000 грн». Перше, що робить тестувальник, — не пише тести, а ставить питання аналітику: 1000 включно? Припустімо, відповідь «так, включно». Тепер структура: два класи еквівалентності (до 1000 — без знижки, від 1000 — зі знижкою), одна межа — 1000. Гроші — високий ризик, тож замість 2-точкового беремо 3-точковий аналіз: 999, 1000, 1001.
Чому саме три точки, видно на двох реалізаціях з одним і тим самим багом «на межі»:
// Реалізація А: втрачено «включно»
function hasFreeShipping(total: number): boolean {
return total > 1000; // off-by-one: треба >=
}
// Реалізація Б: слід невдалого рефакторингу if/else
function hasFreeShippingB(total: number): boolean {
return total === 1000; // мало бути >=
}
| Значення | Очікування | Реалізація А | Реалізація Б |
|---|---|---|---|
| 999 | знижки немає | немає — зелений | немає — зелений |
| 1000 | знижка є | немає — червоний | є — зелений |
| 1001 | знижка є | є — зелений | немає — червоний |
Що дивитися і чому:
- Реалізацію А ловить сама межа (1000). Це той дефект, під який заточений 2-точковий аналіз: умова зсунута на одиницю, і точка 1000 опинилася не в тому класі.
- Реалізацію Б ловить лише 1001 — третя точка. Двоточковий набір (999, 1000) для неї повністю зелений: межа влучена точно, але точково. Саме такі дефекти — аргумент «за» 3-точковий на грошових межах.
- «Типове» значення 5000 не додає нічого. Воно пройде тією самою гілкою, що й 1001. Якщо в сюїті є тест «замовлення на 5000 → доставка безкоштовна» і немає 999–1001 — межі не покриті, скільки б тестів не було.
- Питання «включно?» — теж тест. Якби аналітик відповів «ні, строго більше», весь розклад зсувається: межа валідного класу — 1001, а 1000 — останнє значення без знижки. Мовчазне припущення тут дало б зелені тести навколо чужої гіпотези.
Кейс 2. Пагінація по 20: off-by-one, який видно лише на рівному розмірі сторінки
Список замовлень віддається API посторінково, pageSize = 20. Вимога мовчить про межі, але вони є: 0 елементів, 1, рівно 20, 21. Значення «рівно розмір сторінки» — найгарячіша точка: тут живуть класичні баги «зникла остання позиція» (у коді < замість <= при зрізі) і «порожня друга сторінка» (кількість сторінок порахована як count / pageSize + 1 без урахування рівного ділення).
BVA природно лягає на параметризований API-тест: готуємо рівно N записів, перевіряємо і вміст першої сторінки, і загальну кількість сторінок.
import { test, expect } from '@playwright/test';
const cases = [
{ total: 0, firstPage: 0, pages: 0 }, // порожній стан
{ total: 1, firstPage: 1, pages: 1 }, // мінімальна колекція
{ total: 20, firstPage: 20, pages: 1 }, // рівно сторінка: другої бути не повинно
{ total: 21, firstPage: 20, pages: 2 }, // сторінка + 1: друга з одним елементом
];
for (const { total, firstPage, pages } of cases) {
test(`пагінація: ${total} елементів → ${pages} стор., на першій ${firstPage}`, async ({ request }) => {
await seedOrders(request, total); // фікстура створює рівно total записів
const res = await request.get('/api/orders?page=1&pageSize=20');
const body = await res.json();
expect(body.items).toHaveLength(firstPage);
expect(body.totalPages).toBe(pages);
});
}
Що дивитися і чому:
- Кейс
total: 20— головний. ЯкщоtotalPagesдорівнює 2, а друга сторінка порожня — це той самий fencepost: код порахував «стовпи» замість «прольотів». Якщо на першій сторінці 19 елементів — off-by-one у зрізі, «зникла остання позиція». - Кожне значення — окремий тест з окремим вердиктом. У звіті одразу видно, який бік межі зламався: «на 20 усе добре, на 21 немає другої сторінки» — це вже половина діагнозу для розробника.
- Фікстура має створювати рівно потрібну кількість. Якщо в базі живуть залишки від попередніх прогонів, значення 20 перетворюється на випадкове 20+сміття — і тест перевіряє не межу, а погоду в середовищі. Точний контроль кількості — передумова BVA на колекціях.
Кейс 3. Ліміт 255 символів: межа зустрічає юнікод
Поле «імʼя» з лімітом 255 символів. База: 254, 255, 256 (сусід, межа, сусід за межею) плюс порожній рядок. Але спершу — питання до вимоги: 255 чого саме? Символів, байтів чи кодових одиниць? У JavaScript '😀'.length дорівнює 2, бо емодзі кодується сурогатною парою, тож рядок зі 128 емодзі — це 128 «символів» для людини і 256 кодових одиниць для коду.
const cases = [
{ name: 'a'.repeat(254), valid: true, label: 'межа - 1' },
{ name: 'a'.repeat(255), valid: true, label: 'рівно межа' },
{ name: 'a'.repeat(256), valid: false, label: 'межа + 1' },
// 128 емодзі: 128 символів для людини, 256 кодових одиниць для коду
{ name: '😀'.repeat(128), valid: true, label: 'юнікод на межі' },
];
Що дивитися і чому:
- Кейс з емодзі майже напевно розійдеться з очікуванням. Якщо поле відхиляє 128 емодзі — валідація рахує кодові одиниці, а не символи. Це не обовʼязково баг: це недомовлена вимога, і знахідку треба нести аналітику, а не одразу в баг-трекер.
- Перевіряй і клієнт, і сервер. Фронтенд може різати по символах, а колонка в базі — по байтах: значення проходить форму й падає (або мовчки обрізається) на запиті. Той самий рядок варто прогнати через API повз UI-валідацію.
- Порожній рядок — окрема межа, а не «ще один невалідний». У нього нерідко своя гілка логіки (обовʼязковість поля проти довжини), і повідомлення про помилку має бути про «поле порожнє», а не про «занадто довге».
Механізм: чому межі притягують баги
- Можу пояснити, чому дефекти концентруються на межах: кожен діапазон у коді — це порівняння, а кожне порівняння — кандидат на помилку на одиницю (off-by-one).
- Знаю три джерела off-by-one: переплутані оператори (
>замість>=), неоднозначні вимоги («до 31 грудня» — включно?), проблема стовпів і прольотів (fencepost error: 10 метрів паркану — 11 стовпів). - Розумію, чому тест із «типовим» значенням не ловить off-by-one: усередині класу всі значення проходять тією самою гілкою коду, а ламається одна точка.
Звʼязок із класами еквівалентності
- Знаю, що BVA — не самостійна техніка, а надбудова: спершу класи еквівалентності, потім точки їхнього стику.
- Розумію, що межі існують лише для впорядкованих значень (числа, довжини, дати, розміри); для «картка / готівка» BVA незастосовний — там таблиці рішень.
- Можу пояснити, чому при 2-точковому BVA окремий тест «типового» 50 необовʼязковий: 1 і 100 самі лежать у валідному класі й покривають його.
2- і 3-точковий аналіз
- Знаю різницю 2-точкового vs 3-точкового BVA: межа + сусід у суміжному класі проти межі + обох сусідів.
- Можу порахувати для поля 1–100: класів 3, меж 2; 2-точковий — 4 тести (0, 1, 100, 101), 3-точковий — 6 (0, 1, 2, 99, 100, 101).
- Можу пояснити, який дефект ловить лише третя точка: точкове влучення на кшталт
total === 1000замістьtotal >= 1000— 999 і 1000 зелені, ламається 1001. - Знаю, коли 3-точковий виправданий: високий ризик межі (гроші, доступи, ліміти безпеки), а не «бо так надійніше» — це в півтора раза більше кейсів на кожну межу.
Інтервали й робота з вимогами
- Знаю різницю закритого
[1; 100], відкритого(1; 100)і напіввідкритого[1; 100)інтервалів і як відкритість зсуває набір тестових значень. - Розумію, що питання «включно чи ні» — це тест вимоги, а не педантичність: мовчазне припущення дає зелені тести навколо зсунутої межі.
Межі поза числами
- Можу назвати типові межі для рядків: порожній, 1 символ, максимальна довжина, максимум + 1 — і питання «довжина в чому: символи, байти чи кодові одиниці?» (
'😀'.length === 2). - Знаю межі колекцій і пагінації: 0, 1 елемент, рівно розмір сторінки, розмір + 1 — і класичні баги «зникла остання позиція», «порожня друга сторінка».
- Памʼятаю багатовимірні межі дат: 28/29/30/31, 29 лютого (2024 існує, 2100 — ні), перехід через північ, переведення годинника, проблема 2038 року в legacy.
- Знаю межі типів понад вимогою: 2 147 483 647 для 32-бітного знакового цілого,
Number.MAX_SAFE_INTEGERу JavaScript. - Можу знайти неявні межі: довжина колонки БД, ліміт тіла запиту на сервері/проксі, ліміти сторонніх API; евристика — круглі для компʼютера числа (255, 256, 1024, 65 535).
BVA в автотестах
- Можу оформити граничні значення параметризованим тестом: значення й очікування — в таблицю, кожне значення — окремий кейс з окремим вердиктом.
- Розумію, чому не можна обʼєднувати кілька невалідних меж в один прогін: якщо валідація зупиняється на першій помилці, друга межа лишається неперевіреною.
Чому дефекти концентруються саме на межах діапазонів, а не «розмазані» рівномірно?
Питання
Що таке аналіз граничних значень (boundary value analysis, BVA)?