Таблицы решений и состояния

Две техники для тех случаев, когда логика запутана условиями «если... но при этом... кроме случая, когда...».

Таблица решений — таблица, в которой столбцы это правила, а строки — условия и вытекающие из них действия. Диаграмма состояний и переходов — модель, описывающая, в каких состояниях может находиться объект и какие события переводят его из одного в другое. Обе превращаются в тест-кейсы почти механически.

Когда классов эквивалентности не хватает

Аналитик приносит требование к бесплатной доставке:

«Доставка бесплатна при заказе от 3000 ₽. Для подписчиков Premium доставка бесплатна всегда. Ещё есть промокод FREESHIP, который тоже делает доставку бесплатной. В остальных случаях доставка стоит 299 ₽.»

Звучит понятно. Но задай простой вопрос: что происходит, если клиент с Premium вводит промокод FREESHIP при заказе на 5000 ₽? Доставка и так бесплатна — промокод сгорает? Остаётся у клиента? Показываем ли мы предупреждение? В требовании об этом ни слова. И такой пробел обнаружится в проде через месяц — жалобой в поддержку.

Таблица решений — инструмент, который вытаскивает такие дыры до написания кода. Механика проста: у нас три условия, у каждого два состояния (да/нет), значит комбинаций 2³ = 8. Выписываем все восемь и для каждой честно отвечаем, что должна сделать система.

Таблица решений

УсловияП1П2П3П4П5П6П7П8
Сумма от 3000 ₽ДаДаДаДаНетНетНетНет
Подписка PremiumДаДаНетНетДаДаНетНет
Промокод FREESHIPДаНетДаНетДаНетДаНет
Действия
Доставка бесплатна
Списать промокод???
Стоимость доставки0 ₽0 ₽0 ₽0 ₽0 ₽0 ₽0 ₽299 ₽

Смотри, что произошло. Из восьми правил в четырёх (П1, П3, П5) в строке «Списать промокод» стоит вопросительный знак. Это не лень тестировщика — это точный, локализованный вопрос к аналитику: «В правилах П1, П3 и П5 промокод фактически не нужен. Сгорает он или нет?» Одна таблица — и вместо расплывчатого «а что если» у тебя конкретный список пробелов в требованиях.

Порядок построения таблицы всегда один:

  1. Выпиши все условия (то, что влияет на решение) — каждое отдельной строкой.
  2. Посчитай число правил: 2 в степени количества условий (для условий с более чем двумя значениями — произведение).
  3. Заполни комбинации механически: первое условие меняется через 4 столбца, второе через 2, третье через 1 — тогда ничего не пропустишь.
  4. Для каждого правила запиши действия системы. Не можешь — ставь «?» и иди к аналитику.
  5. Схлопни правила, где часть условий не влияет на результат.
  6. Каждое оставшееся правило превращается ровно в один тест-кейс.

Схлопывание: из восьми правил — четыре теста

После разговора с аналитиком выяснилось: промокод сгорает только если реально применился. Теперь таблица схлопывается — видно, что П2, П4 и П6 дают одинаковый результат, и П1, П3, П5 тоже:

ТестСитуацияОжидаемый результат
ТС-1Доставка уже бесплатна (сумма или Premium), промокода нет0 ₽, промокод не трогаем
ТС-2Доставка уже бесплатна, промокод FREESHIP введён0 ₽, промокод НЕ списан, показано «Промокод не потребовался»
ТС-3Сумма ниже 3000, не Premium, промокод FREESHIP0 ₽, промокод списан
ТС-4Сумма ниже 3000, не Premium, промокода нет299 ₽

Четыре теста вместо восьми, ноль неопределённости, каждый тест защищает конкретное бизнес-правило. Плюс к этому — все граничные значения из первого урока никуда не делись: сумму 3000 ₽ надо проверить в парах 2999,99 / 3000,00.

Диаграмма состояний и переходов

Второй тип «запутанной» логики — тот, где важно не сочетание флажков, а история объекта. Самый узнаваемый пример: статус заказа.

created --pay--> paid --pack--> packed --ship--> shipped --deliver--> delivered
   |                |
 cancel           refund
   |                |
   +-------+--------+
           v
       cancelled

Диаграмма красивая, но для тестирования удобнее её табличная форма — таблица переходов. Строки: состояния. Столбцы: события. В ячейке — новое состояние либо прочерк, если переход недопустим.

Состояние \ Событиеpaypackshipdelivercancelrefund
createdpaidcancelled
paidpackedcancelled
packedshipped
shippeddelivered
delivered
cancelled

Теперь самое интересное. В таблице 6 состояний × 6 событий = 36 ячеек, из которых заполнено только 6. Оставшиеся 30 прочерков — это 30 негативных тест-кейсов, и каждый из них проверяет, что система корректно откажет. Отменить уже доставленный заказ. Оплатить отменённый. Отгрузить неоплаченный. Именно здесь, в прочерках, живут самые дорогие баги — те, что стоят компании реальных денег.

Уровни покрытия

  • Покрытие состояний — каждое состояние достигнуто хотя бы раз. Слабейший уровень, годится для дымового теста.
  • Покрытие переходов (0-switch) — каждый валидный переход выполнен хотя бы раз. Рабочий минимум для любого сервиса со статусами.
  • Покрытие пар переходов (1-switch) — каждая последовательность из двух переходов подряд. Ловит баги, где система «помнит» предыдущий шаг: например, отменённый после оплаты заказ должен вернуть деньги, а отменённый до оплаты — нет.
  • Негативные переходы — все прочерки в таблице. Обязательны для денег, доступов и всего, что нельзя откатить.

Как это работает

Модель состояний в коде обычно и правда живёт в виде словаря переходов. Запусти этот пример — он в точности повторяет таблицу выше и показывает, как ведёт себя корректная реализация:

transitions = {
    'created':   {'pay': 'paid', 'cancel': 'cancelled'},
    'paid':      {'pack': 'packed', 'refund': 'cancelled'},
    'packed':    {'ship': 'shipped'},
    'shipped':   {'deliver': 'delivered'},
    'delivered': {},
    'cancelled': {},
}

def run(events):
    state = 'created'
    for event in events:
        allowed = transitions[state]
        if event not in allowed:
            return 'ОТКАЗ: событие ' + event + ' недопустимо в состоянии ' + state
        state = allowed[event]
    return 'OK, итоговое состояние: ' + state

print(run(['pay', 'pack', 'ship', 'deliver']))
print(run(['pay', 'cancel']))
print(run(['cancel', 'pay']))
print(run(['pack']))

Результат:

OK, итоговое состояние: delivered
ОТКАЗ: событие cancel недопустимо в состоянии paid
ОТКАЗ: событие pay недопустимо в состоянии cancelled
ОТКАЗ: событие pack недопустимо в состоянии created

Первый вызов — «счастливый путь», покрытие всех валидных переходов одной цепочкой. Остальные три — негативные тесты из прочерков таблицы. Обрати внимание на вторую строку вывода: оплаченный заказ нельзя cancel, для него есть refund. Разница между «отменить» и «вернуть деньги» — ровно то, что диаграмма состояний заставляет проговорить вслух, а обычный чек-лист пропускает.

Проверять переходы через интерфейс мало. Реальная дыра почти всегда на бэкенде: фронтенд просто не рисует кнопку «Оплатить» для отменённого заказа, а API-эндпоинт при этом честно принимает такой запрос. Поэтому негативные переходы тестируй запросами к API напрямую.

Частые ошибки

  • Пропускать «невозможные» комбинации в таблице решений. «Ну этого же не бывает» — бывает, если поле можно подделать в запросе. Выписывай все 2ⁿ правил и только потом отсекай, письменно объяснив почему.
  • Схлопывать правила до того, как заполнены действия. Схлопывание — последний шаг. Если сделать его раньше, ты просто не заметишь пробел в требованиях.
  • Тестировать только «счастливый путь» по диаграмме. Шесть валидных переходов покрываются одной цепочкой за пять минут. Тридцать негативных — вот настоящая работа.
  • Забывать про состояния, в которые нельзя попасть из UI. Заказ может уйти в cancelled по таймауту оплаты, из админки, по запросу поддержки. Все входы в состояние — отдельные тесты.
  • Считать таблицу решений разовым артефактом. Требования меняются, добавляется четвёртое условие — правил становится 16. Таблица должна жить рядом с тест-кейсами и обновляться вместе с ними.
  • Проверять переходы только через интерфейс. Кнопки нет — это не значит, что действия нет. Дублируй негативные переходы на уровне API.

Итоги

  • Таблица решений — для логики «несколько условий → разные действия». Строки: условия и действия, столбцы: правила. Число правил — 2 в степени числа условий.
  • Главная ценность таблицы — не тесты, а найденные пробелы и противоречия в требованиях до написания кода. Ставь «?» там, где требование молчит.
  • После уточнения правила схлопываются: каждое оставшееся становится одним тест-кейсом.
  • Диаграмма состояний — для логики, где важна история объекта: статусы заказа, платежа, заявки, сессии.
  • Табличная форма диаграммы удобнее для тестов: заполненные ячейки — позитивные тесты, прочерки — негативные, и их почти всегда больше.
  • Уровни покрытия: состояния → переходы (0-switch) → пары переходов (1-switch) → недопустимые переходы.
  • Обе техники комбинируются с классами эквивалентности и границами: они дают структуру тестов, а границы — конкретные значения.
Проверьте себя
1. В таблице решений четыре независимых условия, каждое со значениями «да»/«нет». Сколько правил (столбцов) будет в полной таблице?
A8
B16
C4
D12
2. В таблице переходов заказа 6 состояний и 6 событий, валидных переходов всего 6. О чём говорят остальные 30 ячеек?
AЭто ошибка в модели — таблица заполнена неправильно
BЭто 30 негативных тест-кейсов: система должна корректно отказать в таких переходах
CЭти комбинации можно не тестировать, они недостижимы
DЭто состояния, которые нужно добавить в диаграмму