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

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

    Граничні значення

    Зміст

    Якщо баг десь і живе, то найімовірніше — на межі. Поле приймає від 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 — не самостійна техніка з нуля, а надбудова над розбиттям на класи еквівалентності. Спершу ділиш вхід на класи, потім дивишся на точки, де класи стикаються. Важливе обмеження: межі існують лише там, де значення впорядковані — числа, довжини, дати, розміри. У класу «спосіб оплати: картка / готівка» меж немає, там працюють інші техніки (наприклад, таблиці рішень).

    Ні

    Так

    Звичайний

    Високий

    Вимога: діапазон, ліміт, дата

    Розбити на класи еквівалентності

    Значення впорядковані?

    BVA незастосовний - інші техніки

    Знайти точки стику класів

    Уточнити: межа включена чи ні?

    Ризик високий?

    2-точковий: межа + сусід за межею

    3-точковий: межа + обидва сусіди

    Ні

    Так

    Звичайний

    Високий

    Вимога: діапазон, ліміт, дата

    Розбити на класи еквівалентності

    Значення впорядковані?

    BVA незастосовний - інші техніки

    Знайти точки стику класів

    Уточнити: межа включена чи ні?

    Ризик високий?

    2-точковий: межа + сусід за межею

    3-точковий: межа + обидва сусіди

    Крок «уточнити межу» — не формальність. Якщо у вимозі написано «до 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-точковий BVA0, 1, 100, 1014
    3-точковий BVA0, 1, 2, 99, 100, 1016

    Зверни увагу: 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 — офіційний патерн параметризації, яким граничні значення перетворюються на табличні автотести.