Что такое покрытие операторов и решений?
Покрытие операторов и решений — техники тест-дизайна белого ящика (структурные), измеряющие, насколько тщательно тест-кейсы выполняют исходный код. В отличие от техник чёрного ящика, фокусирующихся на требованиях, эти техники фокусируются на структуре кода.
Почему важно покрытие кода
Код, который никогда не выполняется при тестировании — код, который никогда не проверен. Метрики покрытия говорят:
- Какие строки кода ваши тесты реально выполняют
- Какие ветви точек решений остаются непротестированными
- Где добавить тесты для лучшего структурного покрытия
Покрытие операторов (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%
Связь: Покрытие решений включает покрытие операторов
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 | Вход | D1 | D2 | D3 | Возврат |
|---|---|---|---|---|---|
| TC1 | age = -5 | И | - | - | «Невалидный» |
| TC2 | age = 10 | Л | И | - | «Несовершеннолетний» |
| TC3 | age = 30 | Л | Л | И | «Взрослый» |
| TC4 | age = 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 | Вход | D1 | D2 | Операторы |
|---|---|---|---|---|
| TC1 | [] | Л (пропуск) | - | S1, S2, S4 |
| TC2 | [5, -3] | И (вход) | И, Л | S1, S2, S3, S4 |
2 тест-кейса для 100% покрытия решений.
Продвинутый анализ покрытия
Пробелы покрытия и их значение
| Тип пробела | Значение | Риск |
|---|---|---|
| Непокрытый оператор | Существует код, который ни один тест не выполняет | Мёртвый код или непротестированная логика |
| Непокрытая ветвь True | Условие никогда не было истинным | Позитивный путь не протестирован |
| Непокрытая ветвь False | Условие никогда не было ложным | Путь ошибки не протестирован |
Ограничения покрытия операторов и решений
Что 100% покрытие НЕ гарантирует:
- Отсутствующий код. Покрытие измеряет существующее, а не отсутствующее. Пропущенная проверка на
nullне покажется как непокрытая. - Дефекты, зависящие от данных.
if (x > 0)с x=1 и x=-1 даёт 100%, но пропускает граничный дефект если условие должно быть>=. - Комбинационные дефекты. Два независимых решения могут взаимодействовать неожиданно.
- Нефункциональные проблемы. Производительность, безопасность и удобство невидимы для покрытия кода.
Реальный пример: Обработка платежей
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% покрытия решений:
| TC | amount | method | currency | Покрывает |
|---|---|---|---|---|
| TC1 | -10 | любой | любая | D1-И |
| TC2 | 100 | credit_card | USD | D1-Л, D2-И, D4-Л |
| TC3 | 100 | bank_transfer | EUR | D1-Л, D2-Л, D3-И, D4-И |
| TC4 | 100 | paypal | USD | D1-Л, D2-Л, D3-Л |
Инструменты для измерения покрытия
| Язык | Инструмент | Команда |
|---|---|---|
| Python | coverage.py | coverage run -m pytest && coverage report |
| JavaScript | Istanbul/nyc | nyc mocha |
| Java | JaCoCo | Интеграция с 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}
Задания:
- Определите все решения и их исходы
- Спроектируйте минимальный набор тестов для 100% покрытия решений
- Рассчитайте покрытие операторов для вашего набора
Подсказка
4 точки решения (4 оператора if), каждая с исходами True и False = 8 исходов. Ранние возвраты означают, что некоторые решения достижимы только когда предыдущие ложны.
Решение
Решения: D1: len < 8 (И/Л), D2: not заглавная (И/Л), D3: not цифра (И/Л), D4: len > 64 (И/Л)
Минимальный набор (5 тест-кейсов):
| TC | Вход | D1 | D2 | D3 | D4 | Возврат |
|---|---|---|---|---|---|---|
| TC1 | "корот" | И | - | - | - | Слишком короткий |
| TC2 | "всемаленькие1" | Л | И | - | - | Нужна заглавная |
| TC3 | "Бездвенадцать" | Л | Л | И | - | Нужна цифра |
| TC4 | "A" + "a"*64 + "1" | Л | Л | Л | И | Слишком длинный |
| TC5 | "ValidPass1" | Л | Л | Л | Л | Валидный |
Покрытие операторов: 100% Покрытие решений: 100%
Нужно 5 тест-кейсов (не 4), потому что ранние возвраты делают некоторые решения достижимыми только когда предыдущие ложны.
Советы профессионала
- Используйте покрытие как ориентир, не как цель. 100% покрытие не означает 100% качество. Но низкое покрытие определённо означает низкую уверенность.
- Фокусируйтесь на покрытии решений, а не операторов. Это более сильный критерий, требующий минимум дополнительных усилий.
- Исследуйте непокрытые ветви. Иногда непокрытый код — мёртвый код, который нужно удалить. Иногда — критическая обработка ошибок, нуждающаяся в тестах.
- Комбинируйте с техниками чёрного ящика. Покрытие говорит, какой код выполнился; EP/BVA — какие значения тестировать. Используйте оба для максимальной эффективности.
- Ставьте реалистичные цели. 80% покрытия практично для большинства проектов. Последние 20% часто включают обработчики ошибок и платформо-специфичный код, которые трудно вызвать в unit-тестах.