От кода к графам
Тестирование потока управления использует структуру кода — ветвления, циклы и последовательности — как основу для проектирования тестов. Главный инструмент — граф потока управления (CFG), который визуально представляет все возможные пути выполнения.
В отличие от техник чёрного ящика, игнорирующих реализацию, тестирование потока управления — техника белого ящика, требующая доступа к исходному коду.
Построение графа потока управления
Базовые элементы
Каждый CFG состоит из:
- Начальный узел: Точка входа функции
- Конечный узел: Точка(и) выхода
- Узлы обработки: Последовательные операторы
- Узлы решений: Точки ветвления — if, switch, тернарный оператор
- Узлы соединения: Где ветви сливаются
- Рёбра: Направленные стрелки, показывающие поток
Последовательные операторы
a = 1
b = 2
c = a + b
Это один узел — ветвления нет.
Структуры If-Else
if condition:
do_something()
else:
do_other()
result = finish()
Циклы
while condition:
process()
post_loop()
Обратное ребро от тела цикла к узлу условия создаёт цикл в графе.
Стратегии тестирования циклов
Циклы — наиболее подверженные ошибкам структуры. Систематический подход тестирует циклы на границах:
Тестирование простого цикла (Loop Testing Бейзера)
Для цикла, итерирующего от 0 до N раз:
| Тест | Итерации | Зачем |
|---|---|---|
| 1 | 0 (пропуск цикла) | Тестирует условие охраны с немедленным false |
| 2 | 1 | Минимальное выполнение — ловит ошибки инициализации |
| 3 | 2 | Тестирует переход от первой ко второй итерации |
| 4 | N-1 | Чуть меньше максимума |
| 5 | N | Максимум итераций — тестирует условие завершения |
| 6 | N+1 (если возможно) | За пределами максимума — тестирует overflow |
Тестирование вложенных циклов
- Начните с самого внутреннего цикла. Зафиксируйте внешние циклы на минимуме. Тестируйте внутренний цикл простым методом.
- Двигайтесь наружу по одному уровню.
- Тестируйте взаимодействия, запуская внешний цикл с внутренним на граничных значениях.
Последовательные циклы
Два цикла подряд: тестируйте каждый независимо, если второй не использует данные первого.
Доминаторы и постдоминаторы
Доминатор: Узел A доминирует над B, если каждый путь от входа до B проходит через A.
Постдоминатор: Узел A постдоминирует над B, если каждый путь от B до выхода проходит через A.
Эти концепции помогают определить:
- Заголовки циклов
- Естественные циклы
- Критические рёбра
Практическое построение CFG
- Пронумеруйте операторы или сгруппируйте последовательные в блоки
- Отметьте точки решений (if, while, for, switch, try/catch)
- Нарисуйте рёбра от каждого блока к его преемникам
- Отметьте обратные рёбра для циклов
- Проверьте: Вход должен достигать каждого узла; каждый узел должен достигать выхода
Пример: Обработка заказа
def process_order(order):
if not order.items:
return {"error": "Empty order"}
total = 0
for item in order.items:
if item.quantity <= 0:
continue
total += item.price * item.quantity
if total > 0:
tax = total * 0.1
return {"total": total + tax}
else:
return {"error": "Invalid total"}
Упражнение: Построение и тестирование CFG
Задача 1
Нарисуйте CFG и выведите тест-кейсы:
def authenticate(username, password, two_factor_code):
user = find_user(username)
if user is None:
return "USER_NOT_FOUND"
if not verify_password(user, password):
user.failed_attempts += 1
if user.failed_attempts >= 3:
lock_account(user)
return "ACCOUNT_LOCKED"
return "WRONG_PASSWORD"
if user.requires_2fa:
if not verify_2fa(user, two_factor_code):
return "INVALID_2FA"
create_session(user)
return "SUCCESS"
Решение
Цикломатическая сложность: 4 решения + 1 = 5
Тест-кейсы (базисные пути):
| # | username | password | 2fa_code | Путь | Ожидаемый |
|---|---|---|---|---|---|
| 1 | “unknown” | any | any | user is None | USER_NOT_FOUND |
| 2 | “valid” | “wrong” | any | неверный пароль, попытки < 3 | WRONG_PASSWORD |
| 3 | “valid” | “wrong” | any | неверный пароль, попытки >= 3 | ACCOUNT_LOCKED |
| 4 | “valid_2fa” | “correct” | “wrong” | ошибка 2FA | INVALID_2FA |
| 5 | “valid_2fa” | “correct” | “valid” | всё ОК с 2FA | SUCCESS |
| 6 | “valid_no2fa” | “correct” | any | без 2FA | SUCCESS |
Задача 2
Примените тестирование циклов:
def find_duplicates(items, max_scan=100):
seen = set()
duplicates = []
for i, item in enumerate(items):
if i >= max_scan:
break
if item in seen:
duplicates.append(item)
else:
seen.add(item)
return duplicates
Решение
| # | items | max_scan | Итерации | Тестирует |
|---|---|---|---|---|
| 1 | [] | 100 | 0 | Пустой список |
| 2 | [“a”] | 100 | 1 | Один элемент |
| 3 | [“a”, “a”] | 100 | 2 | Дубликат на второй итерации |
| 4 | [“a”,“b”,“c”,“d”,“e”] | 100 | 5 | Без дубликатов |
| 5 | [“a”,“b”,“a”,“c”,“b”] | 100 | 5 | С дубликатами |
| 6 | [“a”,“b”,“c”] | 2 | 2 | max_scan достигнут |
| 7 | [“a”,“b”,“c”] | 0 | 0 | max_scan=0 |
Анализ CFG на практике
В реальных проектах вы редко рисуете CFG вручную для каждой функции:
- Используйте инструменты. Многие IDE визуализируют поток управления.
- Фокусируйтесь на сложных функциях. V(G) > 10 больше всего выигрывает от анализа CFG.
- Ищите паттерны — глубоко вложенные if-else, циклы с множественными выходами.
- Сначала упрощайте. Если CFG слишком сложен, функция нуждается в рефакторинге.
Ключевые выводы
- Графы потока управления представляют все возможные пути выполнения через код
- Узлы — блоки операторов; рёбра — поток между ними; обратные рёбра создают циклы
- Тестирование циклов по стратегии Бейзера: 0, 1, 2, N-1, N, N+1 итераций
- Вложенные циклы: тестируйте внутренние сначала, затем двигайтесь наружу
- Доминаторы определяют заголовки циклов и критические структурные связи
- Цикломатическая сложность CFG даёт минимальное число тестов базисных путей
- Используйте анализ CFG преимущественно для сложных функций (V(G) > 10)