Попарное тестирование (pairwise)
Что делать, когда полный перебор комбинаций — это 216 тестов, а времени у тебя на двенадцать.
Попарное тестирование (pairwise, all-pairs) — техника, при которой тест-набор покрывает все возможные пары значений каждых двух параметров, но не все комбинации целиком. Размер набора падает в разы, а способность находить дефекты почти не страдает.
Комбинаторный взрыв
Пока мы тестируем одно поле, всё просто: классы эквивалентности плюс границы — и готово. Но реальный экран оформления заказа выглядит иначе. На нём одновременно живут:
- Браузер: Chrome, Firefox, Safari;
- ОС: Windows, macOS, Linux;
- Способ оплаты: Карта, СБП, PayPal, Наличные;
- Доставка: Курьер, Пункт выдачи, Почта;
- Валюта: RUB, USD.
Полный перебор — это произведение: 3 × 3 × 4 × 3 × 2. Посчитай сам, а заодно посмотри, сколько там всего пар значений:
from itertools import product
params = {
'Браузер': ['Chrome', 'Firefox', 'Safari'],
'ОС': ['Windows', 'macOS', 'Linux'],
'Оплата': ['Карта', 'СБП', 'PayPal', 'Наличные'],
'Доставка': ['Курьер', 'Пункт выдачи', 'Почта'],
'Валюта': ['RUB', 'USD'],
}
names = list(params)
full = list(product(*params.values()))
print('Полный перебор:', len(full), 'комбинаций')
pairs = set()
for i in range(len(names)):
for j in range(i + 1, len(names)):
for a in params[names[i]]:
for b in params[names[j]]:
pairs.add((names[i], a, names[j], b))
print('Всего пар значений:', len(pairs))
lower_bound = max(
len(params[names[i]]) * len(params[names[j]])
for i in range(len(names))
for j in range(i + 1, len(names))
)
print('Нижняя граница размера набора:', lower_bound)
Результат:
Полный перебор: 216 комбинаций
Всего пар значений: 89
Нижняя граница размера набора: 12
216 ручных прогонов оформления заказа — это неделя работы. И это ещё скромный пример: добавь язык интерфейса, тип аккаунта и наличие промокода — счёт пойдёт на тысячи. Явление называется комбинаторным взрывом: число комбинаций растёт как произведение, а рабочих часов у тебя линейно.
Почему пар достаточно
Ключевое наблюдение, на котором стоит вся техника: подавляющее большинство дефектов вызывается одним параметром или взаимодействием ровно двух. Исследования дефектов в реальных системах (самые известные — работы NIST по медицинскому ПО, браузерам и серверам) раз за разом показывают одну картину: значительная часть багов срабатывает уже от единственного «неудачного» значения, а вместе с парными взаимодействиями набирается подавляющее большинство. Ситуации вида «баг вылезает только когда Safari И PayPal И Почта И доллары одновременно» существуют, но они редки.
Отсюда прагматичный вывод: не будем гоняться за всеми 216 комбинациями. Обеспечим, чтобы каждая пара значений встретилась хотя бы в одном тесте — все 89 пар из примера выше. Оказывается, для этого хватает примерно дюжины тестов вместо двух сотен.
Заметь и вторую строчку вывода: нижняя граница — 12. Меньше 12 тестов не выйдет физически: параметры «Браузер» (3 значения) и «Оплата» (4 значения) дают 12 пар, а в одном тесте помещается только одна такая пара. Это простая, но полезная проверка здравого смысла: если инструмент выдал набор меньше произведения двух самых «широких» параметров — он врёт.
Строим набор руками
Чтобы увидеть механику, возьмём пример поменьше — три параметра по три значения:
- Аккаунт: Гость, Обычный, Premium;
- Оплата: Карта, СБП, PayPal;
- Доставка: Курьер, Пункт выдачи, Почта.
Полный перебор — 27 комбинаций. Пар значений тоже 27 (три пары параметров по девять сочетаний). А вот набор, который покрывает все 27 пар всего девятью тестами:
| № | Аккаунт | Оплата | Доставка |
| 1 | Гость | Карта | Курьер |
| 2 | Гость | СБП | Пункт выдачи |
| 3 | Гость | PayPal | Почта |
| 4 | Обычный | Карта | Пункт выдачи |
| 5 | Обычный | СБП | Почта |
| 6 | Обычный | PayPal | Курьер |
| 7 | Premium | Карта | Почта |
| 8 | Premium | СБП | Курьер |
| 9 | Premium | PayPal | Пункт выдачи |
Проверь сам: возьми любую пару, скажем «Premium + Почта» — она в тесте 7. «СБП + Курьер» — тест 8. «Обычный + PayPal» — тест 6. Все двадцать семь пар на месте, а тестов девять, то есть в три раза меньше полного перебора. Причём тройка «Гость + PayPal + Пункт выдачи» в набор не попала — и это осознанная жертва, ради которой техника и существует.
Обрати внимание на структуру таблицы: в каждом столбце каждое значение встречается ровно три раза, и значения «съезжают» по диагонали. Это латинский квадрат — самый наглядный способ построить идеальное попарное покрытие для равных по размеру параметров.
Ограничения: не все комбинации существуют
В нашем большом примере есть ловушка: Safari не работает на Windows и Linux. Если тупо скормить параметры генератору, он честно выдаст тест «Safari + Windows» — и ты потратишь полдня, пытаясь его выполнить.
Поэтому любой взрослый инструмент попарного тестирования умеет ограничения (constraints). Самый распространённый — Microsoft PICT, консольная утилита. Модель описывается обычным текстовым файлом:
Браузер: Chrome, Firefox, Safari
ОС: Windows, macOS, Linux
Оплата: Карта, СБП, PayPal, Наличные
Доставка: Курьер, Пункт выдачи, Почта
Валюта: RUB, USD
IF [Браузер] = "Safari" THEN [ОС] = "macOS";
IF [Оплата] = "Наличные" THEN [Доставка] <> "Почта";
pict model.txt > tests.tsv
На выходе — таблица тестов, где невозможных сочетаний просто нет. Второе правило в примере — тоже из жизни: наличными при отправке почтой не расплатишься.
Тот же результат можно получить из Python-скрипта библиотекой allpairspy (её нужно поставить через pip install allpairspy, поэтому запустить прямо здесь не получится):
from allpairspy import AllPairs
parameters = [
['Chrome', 'Firefox', 'Safari'],
['Windows', 'macOS', 'Linux'],
['Карта', 'СБП', 'PayPal', 'Наличные'],
]
def is_valid(row):
# row приходит частично заполненной — проверяем только то, что уже есть
if len(row) >= 2 and row[0] == 'Safari' and row[1] != 'macOS':
return False
return True
for i, case in enumerate(AllPairs(parameters, filter_func=is_valid), 1):
print(i, case)
Как это работает
Инструменты строят так называемый покрывающий массив (covering array). Наивно это делается жадным алгоритмом: выписываем список всех пар, которые нужно покрыть; берём пустой тест и заполняем его значениями так, чтобы он закрыл как можно больше ещё непокрытых пар; вычёркиваем закрытые пары; повторяем, пока список не опустеет. Каждый следующий тест приносит всё меньше новых пар — отсюда характерная «затухающая» кривая: первые тесты дают огромный прирост покрытия, последние добирают одну-две пары.
Более умные алгоритмы (например, IPOG, который живёт внутри PICT и NIST ACTS) строят набор для двух параметров, а затем «наращивают» его по одному параметру, попутно чиня дырки. На практике знать их устройство не нужно — важно понимать смысл: ты меняешь полноту покрытия на размер набора и делаешь это осознанно, а не «потому что времени не было».
И ещё одна вещь, которую многие упускают. Pairwise не решает, какие значения подставлять в параметры, — он решает, как их сочетать. Значения ты по-прежнему выбираешь классами эквивалентности и граничными значениями из прошлого урока. Порядок такой: сначала для каждого параметра выбери 2–4 осмысленных представителя, потом отдай их генератору.
Частые ошибки
- Применять pairwise к бизнес-логике, где важны конкретные сочетания. Если по требованиям «Premium + промокод + сумма выше 3000» даёт особую скидку, эта тройка обязана быть в наборе целиком. Для такой логики нужна таблица решений (следующий урок), а не попарное покрытие.
- Забыть про ограничения. Набор с невозможными комбинациями выглядит внушительно, а исполнить его нельзя. Хуже того: пары, которые «висели» на невозможных тестах, останутся непокрытыми.
- Сунуть в генератор параметры, которые ни на что не влияют. Цвет темы интерфейса не взаимодействует со способом оплаты. Лишний параметр раздувает набор и не добавляет информации — сначала подумай, какие параметры реально пересекаются в коде.
- Брать слишком много значений на параметр. Двадцать стран в списке — это не двадцать классов эквивалентности. Скорее всего, их три: «страна с НДС», «страна без НДС», «страна из санкционного списка».
- Считать, что pairwise = 100% покрытие. Это компромисс, и об этом честно надо говорить команде. Если баг требует тройного взаимодействия — попарный набор его пропустит.
- Смешивать позитивные и негативные значения в одном прогоне. Если в параметр «Оплата» добавить «просроченная карта», генератор размажет отказы по всему набору и половина тестов будет падать на первом шаге, не дойдя до проверки остального.
Итоги
- Комбинаторный взрыв — произведение числа значений параметров. Пять параметров легко дают сотни комбинаций.
- Большинство реальных дефектов вызвано одним параметром или взаимодействием двух — на этом наблюдении и стоит pairwise.
- Попарный набор покрывает все пары значений, сокращая тесты в разы: 216 → примерно дюжина, 27 → 9.
- Нижняя граница размера набора — произведение двух самых «широких» параметров. Полезная проверка на вменяемость.
- Ограничения (constraints) обязательны: без них генератор выдаст невозможные комбинации вроде Safari на Windows.
- Значения для параметров бери классами эквивалентности и границами; pairwise отвечает только за их сочетание.
- Инструменты: PICT (консоль, ограничения, бесплатен),
allpairspy(Python), онлайн-генераторы.