Попарное тестирование (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Курьер
7PremiumКартаПочта
8PremiumСБПКурьер
9PremiumPayPalПункт выдачи

Проверь сам: возьми любую пару, скажем «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), онлайн-генераторы.
Проверьте себя
1. Форма имеет 4 параметра: 3, 3, 4 и 2 значения. Сколько тестов даст полный перебор всех комбинаций?
A12
B36
C72
D24
2. В каком случае попарное тестирование применять НЕ стоит?
AКогда параметров много и они слабо связаны между собой
BКогда по требованиям конкретное сочетание трёх условий даёт особое поведение системы
CКогда нужно сократить матрицу совместимости браузеров и ОС
DКогда полный перебор занимает слишком много времени