Таблицы решений и состояния
Две техники для тех случаев, когда логика запутана условиями «если... но при этом... кроме случая, когда...».
Таблица решений — таблица, в которой столбцы это правила, а строки — условия и вытекающие из них действия. Диаграмма состояний и переходов — модель, описывающая, в каких состояниях может находиться объект и какие события переводят его из одного в другое. Обе превращаются в тест-кейсы почти механически.
Когда классов эквивалентности не хватает
Аналитик приносит требование к бесплатной доставке:
«Доставка бесплатна при заказе от 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 промокод фактически не нужен. Сгорает он или нет?» Одна таблица — и вместо расплывчатого «а что если» у тебя конкретный список пробелов в требованиях.
Порядок построения таблицы всегда один:
- Выпиши все условия (то, что влияет на решение) — каждое отдельной строкой.
- Посчитай число правил: 2 в степени количества условий (для условий с более чем двумя значениями — произведение).
- Заполни комбинации механически: первое условие меняется через 4 столбца, второе через 2, третье через 1 — тогда ничего не пропустишь.
- Для каждого правила запиши действия системы. Не можешь — ставь «?» и иди к аналитику.
- Схлопни правила, где часть условий не влияет на результат.
- Каждое оставшееся правило превращается ровно в один тест-кейс.
Схлопывание: из восьми правил — четыре теста
После разговора с аналитиком выяснилось: промокод сгорает только если реально применился. Теперь таблица схлопывается — видно, что П2, П4 и П6 дают одинаковый результат, и П1, П3, П5 тоже:
| Тест | Ситуация | Ожидаемый результат |
| ТС-1 | Доставка уже бесплатна (сумма или Premium), промокода нет | 0 ₽, промокод не трогаем |
| ТС-2 | Доставка уже бесплатна, промокод FREESHIP введён | 0 ₽, промокод НЕ списан, показано «Промокод не потребовался» |
| ТС-3 | Сумма ниже 3000, не Premium, промокод FREESHIP | 0 ₽, промокод списан |
| ТС-4 | Сумма ниже 3000, не Premium, промокода нет | 299 ₽ |
Четыре теста вместо восьми, ноль неопределённости, каждый тест защищает конкретное бизнес-правило. Плюс к этому — все граничные значения из первого урока никуда не делись: сумму 3000 ₽ надо проверить в парах 2999,99 / 3000,00.
Диаграмма состояний и переходов
Второй тип «запутанной» логики — тот, где важно не сочетание флажков, а история объекта. Самый узнаваемый пример: статус заказа.
created --pay--> paid --pack--> packed --ship--> shipped --deliver--> delivered
| |
cancel refund
| |
+-------+--------+
v
cancelled
Диаграмма красивая, но для тестирования удобнее её табличная форма — таблица переходов. Строки: состояния. Столбцы: события. В ячейке — новое состояние либо прочерк, если переход недопустим.
| Состояние \ Событие | pay | pack | ship | deliver | cancel | refund |
| created | paid | — | — | — | cancelled | — |
| paid | — | packed | — | — | — | cancelled |
| packed | — | — | shipped | — | — | — |
| shipped | — | — | — | delivered | — | — |
| 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) → недопустимые переходы.
- Обе техники комбинируются с классами эквивалентности и границами: они дают структуру тестов, а границы — конкретные значения.