Что такое покрытие операторов и решений?

Покрытие операторов и решений — техники тест-дизайна белого ящика (структурные), измеряющие, насколько тщательно тест-кейсы выполняют исходный код. В отличие от техник чёрного ящика, фокусирующихся на требованиях, эти техники фокусируются на структуре кода.

Почему важно покрытие кода

Код, который никогда не выполняется при тестировании — код, который никогда не проверен. Метрики покрытия говорят:

  • Какие строки кода ваши тесты реально выполняют
  • Какие ветви точек решений остаются непротестированными
  • Где добавить тесты для лучшего структурного покрытия

Покрытие операторов (Statement Coverage)

Определение: Процент исполняемых операторов, выполненных тестовым набором.

Покрытие операторов = (Выполненные операторы / Всего исполняемых операторов) x 100%

Пример:

def calculate_discount(price, is_member):    # Строка 1
    discount = 0                              # Строка 2
    if is_member:                             # Строка 3
        discount = price * 0.10               # Строка 4
    final_price = price - discount            # Строка 5
    return final_price                        # Строка 6

Тест-кейс 1: calculate_discount(100, True) → Выполняет строки 1, 2, 3, 4, 5, 6

Покрытие операторов = 6/6 = 100%

Но мы тестировали только с is_member=True. Мы никогда не проверяли случай False. Это слабость покрытия операторов.

Покрытие решений (Decision/Branch Coverage)

Определение: Процент исходов решений (ветвей true/false), выполненных тестовым набором.

Покрытие решений = (Выполненные исходы решений / Всего исходов решений) x 100%

Для того же кода:

  • Решение на строке 3: is_member → имеет 2 исхода: True и False

Тест-кейс 1: calculate_discount(100, True) → Решение True Тест-кейс 2: calculate_discount(100, False) → Решение False

Покрытие решений = 2/2 = 100%

flowchart TD A[Начало] --> B[discount = 0] B --> C{is_member?} C -->|True ✅ TC1| D[discount = price * 0.10] C -->|False ✅ TC2| E[final_price = price - discount] D --> E E --> F[return final_price]

Связь: Покрытие решений включает покрытие операторов

100% покрытие решений → 100% покрытие операторов (всегда верно) 100% покрытие операторов → 100% покрытие решений (НЕ всегда верно)

Расчёт покрытия: пошагово

def categorize_age(age):               # S1
    if age < 0:                         # D1
        return "Невалидный"             # S2
    elif age < 18:                      # D2
        return "Несовершеннолетний"     # S3
    elif age < 65:                      # D3
        return "Взрослый"               # S4
    else:                               # D3-false
        return "Пожилой"               # S5

Операторы: S1, S2, S3, S4, S5 (всего 5) Решения: D1 (И/Л), D2 (И/Л), D3 (И/Л) (всего 6 исходов)

Минимальные тест-кейсы для 100% покрытия решений:

TCВходD1D2D3Возврат
TC1age = -5И--«Невалидный»
TC2age = 10ЛИ-«Несовершеннолетний»
TC3age = 30ЛЛИ«Взрослый»
TC4age = 70ЛЛЛ«Пожилой»

4 тест-кейса достигают 100% покрытия операторов и решений.

Покрытие в циклах

def sum_positives(numbers):        # S1
    total = 0                       # S2
    for n in numbers:               # D1 (цикл: вход/пропуск)
        if n > 0:                   # D2
            total += n              # S3
    return total                    # S4

Тест-кейсы для 100% покрытия решений:

TCВходD1D2Операторы
TC1[]Л (пропуск)-S1, S2, S4
TC2[5, -3]И (вход)И, ЛS1, S2, S3, S4

2 тест-кейса для 100% покрытия решений.

Продвинутый анализ покрытия

Пробелы покрытия и их значение

Тип пробелаЗначениеРиск
Непокрытый операторСуществует код, который ни один тест не выполняетМёртвый код или непротестированная логика
Непокрытая ветвь TrueУсловие никогда не было истиннымПозитивный путь не протестирован
Непокрытая ветвь FalseУсловие никогда не было ложнымПуть ошибки не протестирован

Ограничения покрытия операторов и решений

Что 100% покрытие НЕ гарантирует:

  1. Отсутствующий код. Покрытие измеряет существующее, а не отсутствующее. Пропущенная проверка на null не покажется как непокрытая.
  2. Дефекты, зависящие от данных. if (x > 0) с x=1 и x=-1 даёт 100%, но пропускает граничный дефект если условие должно быть >=.
  3. Комбинационные дефекты. Два независимых решения могут взаимодействовать неожиданно.
  4. Нефункциональные проблемы. Производительность, безопасность и удобство невидимы для покрытия кода.

Реальный пример: Обработка платежей

def process_payment(amount, method, currency):
    if amount <= 0:                              # D1
        raise ValueError("Невалидная сумма")

    if method == "credit_card":                  # D2
        fee = amount * 0.029
    elif method == "bank_transfer":              # D3
        fee = 1.50
    else:
        raise ValueError("Метод не поддерживается")

    if currency != "USD":                        # D4
        fee += 0.50

    return amount + fee

Минимальный набор для 100% покрытия решений:

TCamountmethodcurrencyПокрывает
TC1-10любойлюбаяD1-И
TC2100credit_cardUSDD1-Л, D2-И, D4-Л
TC3100bank_transferEURD1-Л, D2-Л, D3-И, D4-И
TC4100paypalUSDD1-Л, D2-Л, D3-Л

Инструменты для измерения покрытия

ЯзыкИнструментКоманда
Pythoncoverage.pycoverage run -m pytest && coverage report
JavaScriptIstanbul/nycnyc mocha
JavaJaCoCoИнтеграция с Maven/Gradle
GoВстроенныйgo test -cover

Упражнение: Спроектируйте тесты для покрытия

Сценарий: Проанализируйте функцию и спроектируйте минимальный набор тестов для 100% покрытия решений:

def validate_password(password):
    if len(password) < 8:
        return {"valid": False, "error": "Слишком короткий"}

    has_upper = any(c.isupper() for c in password)
    has_digit = any(c.isdigit() for c in password)

    if not has_upper:
        return {"valid": False, "error": "Нужна заглавная"}

    if not has_digit:
        return {"valid": False, "error": "Нужна цифра"}

    if len(password) > 64:
        return {"valid": False, "error": "Слишком длинный"}

    return {"valid": True, "error": None}

Задания:

  1. Определите все решения и их исходы
  2. Спроектируйте минимальный набор тестов для 100% покрытия решений
  3. Рассчитайте покрытие операторов для вашего набора
Подсказка

4 точки решения (4 оператора if), каждая с исходами True и False = 8 исходов. Ранние возвраты означают, что некоторые решения достижимы только когда предыдущие ложны.

Решение

Решения: D1: len < 8 (И/Л), D2: not заглавная (И/Л), D3: not цифра (И/Л), D4: len > 64 (И/Л)

Минимальный набор (5 тест-кейсов):

TCВходD1D2D3D4Возврат
TC1"корот"И---Слишком короткий
TC2"всемаленькие1"ЛИ--Нужна заглавная
TC3"Бездвенадцать"ЛЛИ-Нужна цифра
TC4"A" + "a"*64 + "1"ЛЛЛИСлишком длинный
TC5"ValidPass1"ЛЛЛЛВалидный

Покрытие операторов: 100% Покрытие решений: 100%

Нужно 5 тест-кейсов (не 4), потому что ранние возвраты делают некоторые решения достижимыми только когда предыдущие ложны.

Советы профессионала

  • Используйте покрытие как ориентир, не как цель. 100% покрытие не означает 100% качество. Но низкое покрытие определённо означает низкую уверенность.
  • Фокусируйтесь на покрытии решений, а не операторов. Это более сильный критерий, требующий минимум дополнительных усилий.
  • Исследуйте непокрытые ветви. Иногда непокрытый код — мёртвый код, который нужно удалить. Иногда — критическая обработка ошибок, нуждающаяся в тестах.
  • Комбинируйте с техниками чёрного ящика. Покрытие говорит, какой код выполнился; EP/BVA — какие значения тестировать. Используйте оба для максимальной эффективности.
  • Ставьте реалистичные цели. 80% покрытия практично для большинства проектов. Последние 20% часто включают обработчики ошибок и платформо-специфичный код, которые трудно вызвать в unit-тестах.