От кода к графам

Тестирование потока управления использует структуру кода — ветвления, циклы и последовательности — как основу для проектирования тестов. Главный инструмент — граф потока управления (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 раз:

ТестИтерацииЗачем
10 (пропуск цикла)Тестирует условие охраны с немедленным false
21Минимальное выполнение — ловит ошибки инициализации
32Тестирует переход от первой ко второй итерации
4N-1Чуть меньше максимума
5NМаксимум итераций — тестирует условие завершения
6N+1 (если возможно)За пределами максимума — тестирует overflow

Тестирование вложенных циклов

  1. Начните с самого внутреннего цикла. Зафиксируйте внешние циклы на минимуме. Тестируйте внутренний цикл простым методом.
  2. Двигайтесь наружу по одному уровню.
  3. Тестируйте взаимодействия, запуская внешний цикл с внутренним на граничных значениях.

Последовательные циклы

Два цикла подряд: тестируйте каждый независимо, если второй не использует данные первого.

Доминаторы и постдоминаторы

Доминатор: Узел A доминирует над B, если каждый путь от входа до B проходит через A.

Постдоминатор: Узел A постдоминирует над B, если каждый путь от B до выхода проходит через A.

Эти концепции помогают определить:

  • Заголовки циклов
  • Естественные циклы
  • Критические рёбра

Практическое построение CFG

  1. Пронумеруйте операторы или сгруппируйте последовательные в блоки
  2. Отметьте точки решений (if, while, for, switch, try/catch)
  3. Нарисуйте рёбра от каждого блока к его преемникам
  4. Отметьте обратные рёбра для циклов
  5. Проверьте: Вход должен достигать каждого узла; каждый узел должен достигать выхода

Пример: Обработка заказа

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

Тест-кейсы (базисные пути):

#usernamepassword2fa_codeПутьОжидаемый
1“unknown”anyanyuser is NoneUSER_NOT_FOUND
2“valid”“wrong”anyневерный пароль, попытки < 3WRONG_PASSWORD
3“valid”“wrong”anyневерный пароль, попытки >= 3ACCOUNT_LOCKED
4“valid_2fa”“correct”“wrong”ошибка 2FAINVALID_2FA
5“valid_2fa”“correct”“valid”всё ОК с 2FASUCCESS
6“valid_no2fa”“correct”anyбез 2FASUCCESS

Задача 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
Решение
#itemsmax_scanИтерацииТестирует
1[]1000Пустой список
2[“a”]1001Один элемент
3[“a”, “a”]1002Дубликат на второй итерации
4[“a”,“b”,“c”,“d”,“e”]1005Без дубликатов
5[“a”,“b”,“a”,“c”,“b”]1005С дубликатами
6[“a”,“b”,“c”]22max_scan достигнут
7[“a”,“b”,“c”]00max_scan=0

Анализ CFG на практике

В реальных проектах вы редко рисуете CFG вручную для каждой функции:

  1. Используйте инструменты. Многие IDE визуализируют поток управления.
  2. Фокусируйтесь на сложных функциях. V(G) > 10 больше всего выигрывает от анализа CFG.
  3. Ищите паттерны — глубоко вложенные if-else, циклы с множественными выходами.
  4. Сначала упрощайте. Если CFG слишком сложен, функция нуждается в рефакторинге.

Ключевые выводы

  • Графы потока управления представляют все возможные пути выполнения через код
  • Узлы — блоки операторов; рёбра — поток между ними; обратные рёбра создают циклы
  • Тестирование циклов по стратегии Бейзера: 0, 1, 2, N-1, N, N+1 итераций
  • Вложенные циклы: тестируйте внутренние сначала, затем двигайтесь наружу
  • Доминаторы определяют заголовки циклов и критические структурные связи
  • Цикломатическая сложность CFG даёт минимальное число тестов базисных путей
  • Используйте анализ CFG преимущественно для сложных функций (V(G) > 10)