Покупатель пишет в поддержку: промокод не сработал. Вы открываете форму заказа и проверяете по одному полю: промокод применяется, скидка постоянного покупателя начисляется, оплата при получении проходит. Каждое поле исправно — а баг живёт в сочетании: промокод пропадает только у постоянного покупателя, выбравшего оплату при получении.
Классы эквивалентности и границы на такое не отвечают — они про одно поле. Отвечают два приёма: таблица решений раскладывает сочетания, когда у каждого свой результат, а попарное тестирование сжимает список, когда сочетаний десятки.
Сорок восемь клеток — все сочетания четырёх условий формы заказа. Подсвеченные двенадцать подобраны так, что любая пара значений встречается хотя бы в одной из них: по три клетки в каждой строке и ровно по одной в каждом столбце. Цена сжатия видна тут же: сочетания из трёх условий покрыты наполовину, и обведённая клетка — одно из непроверенных.
Сочетание — это отдельный объект проверки
Кажется, что если каждое поле проверено, то проверена и форма. Именно на этом баг из начала статьи и держится: за формой стоит не одно сравнение на поле, а цепочка вложенных условий — «если введён промокод, скидка 10%; иначе если покупатель постоянный, 5%; если оплата при получении, скидок нет». Двигая поля по очереди, вы идёте по верхним веткам, а ветка, срабатывающая только на сочетании, не выполняется ни разу: её нечем включить. Вдобавок ветки писали разные люди и в разное время — июньский запрет встал в начало цепочки, и мартовская скидка ниже перестала до неё доходить.
Отсюда выбор приёма: если у каждого сочетания свой результат (сумма скидки, доступное действие), важен каждый столбец — это территория таблицы решений; если условия всего лишь фон, а результат везде один, это территория попарного тестирования.
Таблица решений: пять шагов от требования к проверкам
Требование написано прозой, а проза не перечисляет случаи: она выглядит исчерпывающей ровно до момента, когда её пробуют применить к конкретному заказу.
Промокод даёт скидку 10%. Постоянным покупателям — 5%. При оплате при получении скидки не действуют.
Шагов пять. Выписать условия — вопросы к заказу, от ответа на которые зависит результат: введён ли промокод, постоянный ли покупатель, картой или при получении. Выписать варианты каждого: здесь у всех по два, «да» и «нет»; это те же классы эквивалентности, применённые к условию целиком. Перемножить: 2 × 2 × 2 = 8 строк, и пока список помещается на экран, его выписывают целиком — пропущенное видно именно в выписанном виде. Заполнить действия, беря их из требования, а не из работающей программы; чего в требовании нет, оставляют пустым. Свернуть повторы — про это следующий раздел.
| Промокод введён | Постоянный покупатель | Оплата картой | Скидка |
|---|---|---|---|
| да | да | да | ? |
| да | да | нет | 0% |
| да | нет | да | 10% |
| да | нет | нет | 0% |
| нет | да | да | 5% |
| нет | да | нет | 0% |
| нет | нет | да | 0% |
| нет | нет | нет | 0% |
Свёртка и пустая клетка в требованиях
Восемь строк — восемь проверок, и половина из них про одно и то же: когда оплата не картой, скидка равна нулю при любых остальных ответах. Объединить можно строки с одинаковым результатом, отличающиеся ровно одним условием — оно заменяется прочерком «неважно». Четыре нижние строки схлопываются в одну, восемь превращается в пять. Прочерк значит не «можно не проверять», а «на результат не влияет, возьмите любое значение», и представителя берут самого спорного: покупателя с промокодом и статусом постоянного, который платит при получении. Свёртка — утверждение о требовании: изменится оно — строка развернётся обратно в четыре, поэтому несвёрнутую таблицу держат рядом.
Самое ценное в таблице — не готовые проверки, а клетка, которую нечем заполнить. У нас это первая строка: промокод введён, покупатель постоянный, оплата картой. Требование говорит и про 10%, и про 5%, но молчит, что делать, когда верно и то и другое. Ответов минимум четыре, и каждый где-то реализован: скидки складываются (15%), берётся большая (10%), приоритет у промокода, приоритет у статуса (5%). Разработчик выберет молча тот, который первым пришёл в голову, — поэтому знак вопроса не повод угадать, а находка для вопросов к требованиям: ошибка, найденная в тексте, стоит дешевле всех остальных. Дыры бывают трёх видов: требование молчит — нужен ответ владельца продукта; описано дважды и по-разному — противоречие; сочетания не должно быть вовсе (рассрочка при самовывозе) — его запрещают в интерфейсе.
Откуда берётся сорок восемь
Пока условий три, всё считается в уме. Реальная форма заказа сложнее: промокод — 2 варианта, статус покупателя (новый, обычный, постоянный) — 3, способ оплаты (карта, бонусы, при получении, рассрочка) — 4, доставка (курьер или самовывоз) — 2. Всех сочетаний 2 × 3 × 4 × 2 = 48: условий стало четыре вместо трёх, а сочетаний сорок восемь вместо восьми — каждое новое условие не прибавляет случаи, а умножает их. Галочка «подарочная упаковка» — уже 96, выбор из трёх городов — 288. Если одна проверка оформления занимает минут шесть, сорок восемь — почти пять часов на одну форму. И большинство этих сочетаний пройдут по одним и тем же веткам кода, отличаясь только цифрами.
Попарное покрытие: из сорока восьми в двенадцать
Перебирать всё нельзя, выбирать наугад страшно — нужен критерий, какой набор считать достаточным. «Проверим самые важные» критерием не является: через неделю никто не вспомнит, что считалось важным.
Критерий подсказывает статистика дефектов: заметная часть багов срабатывает от одного значения, вместе с парами набирается основная масса, а дефектов, которым нужно совпадение пяти-шести условий, почти не находится — ошибка выживает там, где встретились две ветки, порядок между которыми никто не согласовал. Отсюда критерий: набор достаточен, если каждая пара значений любых двух условий встречается в нём хотя бы раз.
Нижняя граница считается за секунду: возьмите два самых широких условия, статус покупателя (3 варианта) и способ оплаты (4). Их пар 3 × 4 = 12, каждая обязана встретиться, а в одну строку помещается одна такая пара — значит, строк не может быть меньше двенадцати.
| № | Промокод | Статус | Оплата | Доставка |
|---|---|---|---|---|
| 1 | нет | новый | карта | курьер |
| 2 | есть | новый | бонусы | курьер |
| 3 | нет | новый | при получении | самовывоз |
| 4 | есть | новый | рассрочка | самовывоз |
| 5 | есть | обычный | карта | самовывоз |
| 6 | нет | обычный | бонусы | самовывоз |
| 7 | есть | обычный | при получении | курьер |
| 8 | нет | обычный | рассрочка | курьер |
| 9 | нет | постоянный | карта | курьер |
| 10 | есть | постоянный | бонусы | курьер |
| 11 | нет | постоянный | при получении | самовывоз |
| 12 | есть | постоянный | рассрочка | самовывоз |
Набор проверяется вручную: «есть промокод + самовывоз» — строка 4, «постоянный + при получении» — строка 11. Собирают такие наборы генераторы, самый известный — консольный PICT, и одна их возможность обязательна: запреты вроде «рассрочка недоступна при самовывозе». Без них в набор попадут строки, которые физически нельзя ввести в форму; вы вычеркнете их на ходу и потеряете вместе с ними пары. И последнее: попарный набор — это список входных данных, а не тест-кейсов, ожидаемый результат к каждой строке пишут по той же таблице решений.
Чего попарное покрытие не ловит и когда перебирают всё
У сжатия есть цена: двенадцать строк покрывают все пары — и только пары. Сочетаний из трёх условий по осям «статус + оплата + доставка» ровно 3 × 4 × 2 = 24, а в двенадцати строках их помещается максимум двенадцать: половина троек не проверена ни разу. «Новый покупатель + рассрочка + курьер» в таблице не встречается, и если ошибка сидит именно там, все двенадцать проверок пройдут зелёными.
Поступают с этим тремя способами: дописывают руками три-четыре рискованные строки — где недавно чинили баг, на что жаловался покупатель; поднимают силу покрытия до троек, но нижняя граница тогда 3 × 4 × 2 = 24, вдвое дороже, поэтому так делают для одного узла; или делят форму — расчёт скидки перебирают таблицей решений целиком, а фон вокруг закрывают попарно.
Полный перебор возвращается там, где непроверенное сочетание дороже сэкономленных часов: деньги — скидки, налоги, бонусы, возвраты, где тройка означает неправильную сумму в чеке; права доступа — роль, владелец объекта, состояние, действие, где это чужой заказ у постороннего. И попарное не берут, когда условий мало: три по два варианта дают восемь проверок и пять после свёртки — поход к генератору того не стоит. Рабочее правило: у каждого сочетания свой результат — таблица решений, и столбцы проверяют все; результат везде один и тот же, а условий много — попарное покрытие.
Коротко
- Проверка полей по одному не находит баг сочетания: ветка, срабатывающая только на нём, не выполняется ни разу.
- Таблица решений раскладывает требование на случаи: условия → варианты → перемножить → действие → свернуть повторы.
- Число сочетаний — произведение количеств вариантов, а не сумма: 2 × 3 × 4 × 2 = 48.
- Клетка, которую нечем заполнить, — главная находка таблицы: сочетание уточняют, а не угадывают.
- Попарное покрытие требует каждую пару значений двух любых условий хотя бы раз; меньше двенадцати строк не бывает — это произведение двух самых широких условий, 3 × 4.
- Цена сжатия — тройки: из 24 в набор попадает половина, поэтому деньги и права доступа перебирают полностью.
Что почитать дальше
- Техники тест-дизайна: классы эквивалентности и граничные значения — как выбирать значения для одного поля; сочетания строятся уже из них.
- Ревью требований: техники и хорошие вопросы — что делать с клеткой, которую нечем заполнить.
- Чек-листы и тестовые данные — откуда брать заказы и промокоды для двенадцати строк набора.
- Кроссбраузерное и мобильное тестирование — самый частый случай, где условий десятки, а ожидаемый результат один.