Граничные значения и классы эквивалентности: как проверять на практике
Проверить все возможные значения поля нельзя: их слишком много. Две техники решают эту задачу вместе — классы эквивалентности сокращают перебор, анализ границ говорит, какие значения из оставшихся проверять в первую очередь.
Классы эквивалентности: почему хватает одного значения
Поле «количество товара» принимает числа от 1 до 99. Проверять по очереди 1, 2, 3 и дальше бессмысленно: сервис обрабатывает их одинаково. Все значения, с которыми программа поступает одинаково, образуют класс — и одного представителя класса достаточно.
Классов обычно три: допустимые значения, значения ниже допустимого и выше. К ним добавляются отдельные классы для другого типа данных — пустое поле, строка вместо числа, дробное вместо целого, отсутствие поля вообще. Последний часто забывают, а ведёт он себя иначе, чем пустое значение.
Граница: место, где ломается чаще всего
Ошибки редко живут в середине диапазона. Они живут на краю, потому что там программист выбирал между «больше» и «больше либо равно» — и мог ошибиться на единицу. Эту ошибку так и называют: ошибка на единицу.
ISTQB различает двухточечный и трёхточечный анализ. В двухточечном для каждой границы берут значение самой границы и ближайшего соседнего класса: 0 и 1, затем 99 и 100. Трёхточечный добавляет ближайшее значение внутри допустимого класса: 2 и 98. Он дороже, но обнаруживает больше вариантов ошибки на единицу.
- 0 ниже границы ожидаем отказ
- 1 сама граница ожидаем успех
- 2 внутри диапазона ожидаем успех
- 98 внутри диапазона ожидаем успех
- 99 сама граница ожидаем успех
- 100 выше границы ожидаем отказ
Где границы прячутся в API
Число — самый заметный случай, но далеко не единственный. Границы есть почти у всего, что имеет размер, длину или срок.
- Размер страницы: per_page=0, 1, максимум, максимум + 1. Частая находка — параметр молча игнорируется, и сервис отдаёт всё подряд.
- Длина текста: пустая строка, один символ, максимум по требованиям, максимум + 1. Отдельно — только пробелы: формально строка не пустая.
- Даты периода: начало равно концу, начало позже конца, период длиной в день, будущая дата.
- Деньги: ноль, минимальная сумма, копейка сверх остатка, дробная часть длиннее двух знаков.
- Частота действий: последний разрешённый запрос в окне и следующий за ним — тот, что должен получить отказ.
Как выглядит проверка границы
Смысл в том, чтобы менять ровно одно значение и сравнивать ответы. Если на границе и за границей сервис отвечает одинаково — проверки нет, и это дефект независимо от того, как он выглядит на экране.
GET /products?per_page=50 → 200, в data 50 позиций GET /products?per_page=51 → ожидаем 422 GET /products?per_page=0 → ожидаем 422
Ожидаемый результат для каждой строки берётся из требований, а не из наблюдения. Если в требованиях максимум не задан — это само по себе находка: без верхней границы один запрос может попросить весь каталог разом.
Чего эти техники не ловят
Границы и классы работают с одним полем за раз. Дефекты, которые возникают от сочетания полей — скидка плюс количество плюс остаток на складе, — так не найти: для них нужны другие техники и просто внимание к тому, как значения влияют друг на друга.
Ещё они ничего не говорят о правах доступа и о том, что происходит при повторе одного и того же запроса. Это отдельные виды проверок, и начинать стоит с границ только потому, что они дают больше всего находок за меньшее время.
Как получить набор проверок из требования
Записывай классы до конкретных значений. Для поля “скидка — целое число от 0 до 30 включительно” классы такие: допустимое целое, меньше 0, больше 30, дробное, строка, null, поле отсутствует. Границы применимы только к упорядоченным классам; строка и null — отдельные классы, но не числовые соседи диапазона.
-1 → ниже диапазона, отказ\n0 → нижняя граница, успех\n1 → внутри, успех (только 3-value BVA)\n29 → внутри, успех (только 3-value BVA)\n30 → верхняя граница, успех\n31 → выше диапазона, отказ\n2.5, "10", null, missing → отдельные классы
Один представитель класса достаточен только при обоснованном предположении, что обработка действительно одинакова. Если ноль имеет бизнес-смысл, отрицательные значения проходят другой валидатор, а 30 включает отдельное правило промоакции, это уже разные классы.
Где техника ломается и чем её дополнить
Эквивалентность — модель, а не свойство, которое система обязана соблюдать. Пересматривай классы после каждой находки: дефект часто показывает, что внутри предполагаемого класса есть скрытая граница.
- Сочетания полей: используй decision table или pairwise, если результат зависит от нескольких условий.
- Переходы статусов: используй state transition testing, а не набор независимых значений.
- Права и владение объектом: строй матрицу actor × action × object.
- Время и даты: уточняй timezone, включительность границы и переход летнего времени.
- Деньги: уточняй единицу хранения, округление, валюту и допустимую точность до выбора чисел.
Разбор: суточный лимит перевода 10 000
Одно поле amount не описывает весь риск. Лимит относится к накопленной сумме за день, поэтому тест зависит от предыдущих операций и границы календарного окна. Классы и значения нужно выводить из точной формулировки: валюты, часового пояса, включительности границы и того, считаются ли отклонённые переводы.
- Выяснить единицы денег и окно лимита.
- Разделить состояния по накопленной сумме.
- Проверить точную границу и один минимальный шаг за ней.
- Сверить ответ, баланс и историю.
Сначала запиши контракт, не выбирай числа наугад
Предположим, сумма хранится в копейках, лимит 10 000 RUB включительно, день считается по UTC, только успешные исходящие переводы входят в накопление. Если продукт считает день по часовому поясу клиента или курс конвертации используется на момент отправки, набор тестов меняется. Эти уточнения надо получить до багрепорта: иначе 10 000.01 может оказаться корректным по другому правилу.
Выдели классы по состоянию, а не только по параметру
Для нового пользователя первый перевод 1 RUB и 10 000 RUB — допустимые варианты; 0, отрицательное, дробная копейка и 10 000.01 — другие классы. Но если уже отправлено 9 999 RUB, перевод 1 RUB должен пройти, а 1.01 RUB — отклониться. Здесь граница относится к сумме «уже потрачено + новый перевод», а не к новому amount. Отдельно проверь, что отказ не списал деньги и не увеличил счётчик.
Проверь соседние временные точки
Операция непосредственно до 00:00 UTC и сразу после неё должна попасть в разные окна, если правило календарное. При rolling 24 hours ожидание другое. Запросы нужно фиксировать с точным временем и timezone, иначе тест невозможно повторить. Для автоматизации используй управляемые часы или подготовленное состояние, а не ожидание реальной полуночи. Летнее время важно, если окно задано локальной зоной, но не если оно строго UTC.
Проверь сочетание с конкурентностью
Два одновременных перевода по 6 000 RUB на пустом счётчике каждый отдельно выглядит допустимым, но вместе превышают лимит. Это уже не чистая BVA: нужен тест конкуренции и атомарности обновления счётчика. Запиши инвариант: успешных переводов за окно не больше 10 000 RUB суммарно. Разделение техник помогает не делать ложный вывод, будто пара обычных граничных запросов закрыла весь риск.
Spent Request Total Expected\n0.00 10000.00 10000.00 accept; debit once\n0.00 10000.01 10000.01 reject; no debit\n9999 1.00 10000.00 accept\n9999 1.01 10000.01 reject; counter unchanged\n0.00 6000+6000 concurrent at most one succeeds
Что добавить в тест-кейс?
Предусловие с накопленной суммой и часовым поясом, точную единицу денег, ожидаемый код и сообщение, проверку баланса, истории и счётчика после каждого отказа. Если контракт не отвечает, считать ли отменённый перевод частью лимита, запиши вопрос аналитикам, а не придуманный expected.
Второй кейс: промокод на корзину с двумя условиями
Условие акции: скидка 10% действует только на книги, если корзина не меньше 1 000 RUB и пользователь не применял этот код раньше. Проверка одного поля quantity здесь не поможет. Нужно выбрать сочетания условий, а затем границы суммы корзины и числа использований.
Выпиши условия независимо
У нас три фактора: стоимость подходящих книг относительно 1 000 RUB, наличие неподходящих товаров и история использования кода. Не подменяй “сумму корзины” суммой всех товаров без уточнения: возможно, порог считается только по книгам. Сначала спроси владельца правила, учитывается ли доставка и применяются ли возвраты к статусу “уже использован”. Без этого тест ожидаемой суммы может быть неверным.
Построй таблицу решений
Минимум нужны строки: книги ниже порога и код новый; ровно на пороге и код новый; выше порога и код уже использован; порог достигнут только за счёт неподходящего товара. Для каждой строки укажи accept/reject, итоговую скидку и изменение счётчика использования. Если условия независимы, проверь по одной строке, где каждое условие делает результат иным, а не все восемь комбинаций вслепую.
Найди границы с точностью до копейки
При пороге 1 000 RUB проверь 999.99, 1 000.00 и 1 000.01 для подходящих товаров. Если API хранит целые копейки, сравни 99999, 100000, 100001 в сырых данных. Отдельно проверь, округляется ли скидка на каждую позицию или на общую сумму: два товара по 5 копеек могут дать разный результат. Ожидаемую сумму вычисляй независимо от ответа.
Проверь состояние до и после отказа
Отклонённый код не должен помечаться использованным. Успешная покупка должна изменить состояние ровно один раз; отмена заказа может вернуть право на код только если это записано в правиле. После каждой попытки перечитай корзину и историю промокодов. Иначе тест может увидеть правильное сообщение об ошибке, но пропустить скрытое изменение лимита.
Добавь конкурентный запрос отдельно
Два параллельных checkout с одним одноразовым промокодом не должны оба получить скидку, если правило “один раз на пользователя”. Это тест состояния и атомарности, не BVA. Укажи инвариант “успешное применение не более одного” и сверяй не только ответы, но и финальные заказы и счётчик. Так видно, где заканчивается техника границ и начинается тестирование переходов.
Eligible books Other goods Used before Expected\n999.99 0 no reject\n1000.00 0 no 10% books\n900.00 200.00 no contract decision\n1000.01 0 yes reject; no counter change\n1000.00 0 no + no only one parallel success
Какая техника нужна?
Классы и BVA для суммы, таблица решений для сочетания товара, порога и статуса кода, переходы состояния для повторного использования. У каждой техники свой вопрос; вместе они дают меньший и более обоснованный набор, чем десятки случайных корзин. Важно не объявлять каждую комбинацию граничным анализом. Если скидка зависит от роли покупателя, минимальной суммы и того, использован ли промокод, проверка 999,99/1000/1000,01 покрывает только порог. Она не докажет, что промокод нельзя применить второй раз и что сотрудник с запрещённой ролью не получит льготу. Для каждой строки таблицы решений фиксируй ожидаемую сумму и состояние кода после запроса; иначе ответ 422 может выглядеть правильным при ошибочно потраченном одноразовом коде. Когда правило округления неизвестно, не выбирай expected из текущего ответа сервера: уточни у владельца продукта порядок скидки, налогов и округления. Если валюта хранится в минимальных единицах, проверяй точные целые числа, а не результат вычисления с плавающей точкой. Повтори критичную строку через API и интерфейс, чтобы увидеть расхождение между расчётом и отображением.
Частые вопросы о границах
Всегда ли нужно брать три значения у границы?
Нет. Двухточечный BVA берёт границу и ближайшее значение соседнего класса; трёхточечный добавляет значение с другой стороны. Выбор зависит от риска и нужной силы покрытия.
Пустая строка и отсутствующее поле — один класс?
Не обязательно. Пустая строка передана явно, null может означать отсутствие значения, а missing запускает default или проверку required. Пока контракт не доказывает одинаковую обработку, проверяй их отдельно.
Можно ли применять границы к enum?
У enum нет естественного числового порядка, поэтому BVA обычно неприменим. Выдели допустимые и недопустимые классы, регистр, unknown value и отсутствие поля.
Техника запоминается не с первого прочтения, а после того, как одну и ту же границу проверишь в фильтре, в размере страницы и в суточном лимите — и увидишь, что это одна и та же проверка.