Что такое тестирование белого ящика?
Тестирование белого ящика (white-box testing) — также называемое структурным тестированием, тестированием стеклянного или прозрачного ящика — это подход, при котором тесты разрабатываются на основе внутренней структуры программного обеспечения. Тестировщик имеет полный доступ к исходному коду, архитектуре и деталям реализации.
Представьте, что вы осматриваете внутренности часов. Вместо того чтобы проверять, показывают ли стрелки правильное время (чёрный ящик), вы исследуете каждую шестерёнку, пружину и механизм, чтобы убедиться в их корректной работе.
White-box тестирование отвечает на вопрос: «Работает ли каждая часть кода так, как реализована?»
Кто выполняет тестирование белого ящика?
White-box тестирование выполняют преимущественно:
- Разработчики при юнит-тестировании — они знают написанный ими код
- SDET (Software Development Engineers in Test) — умеют читать и анализировать продакшн-код
- QA-инженеры с навыками программирования при интеграционном или системном тестировании
Требуется доступ к исходному коду и способность его понимать. Тестировщик, не умеющий читать код, не сможет эффективно выполнять white-box тестирование.
Техники покрытия кода
Покрытие кода (code coverage) измеряет, какая часть исходного кода была задействована тестами. Разные критерии покрытия обеспечивают разные уровни тщательности.
Каждый возможный путь] BC[Покрытие ветвей
Каждый результат решения] SC[Покрытие операторов
Каждая строка выполнена] PC -->|включает| BC BC -->|включает| SC style PC fill:#ef4444,color:#fff style BC fill:#f97316,color:#000 style SC fill:#22c55e,color:#000
Диаграмма показывает иерархию включения: достижение покрытия путей гарантирует покрытие ветвей, которое в свою очередь гарантирует покрытие операторов. Но не наоборот.
Покрытие операторов (Statement Coverage)
Покрытие операторов (также называемое покрытием строк) измеряет процент исполняемых операторов, выполненных тестами.
Формула: Покрытие операторов = (Выполненные операторы / Всего операторов) × 100%
Рассмотрим функцию:
def calculate_discount(price, is_member):
discount = 0 # Оператор 1
if is_member: # Оператор 2
discount = price * 0.1 # Оператор 3
final_price = price - discount # Оператор 4
return final_price # Оператор 5
Тест 1: calculate_discount(100, True) выполняет операторы 1, 2, 3, 4, 5 → 5/5 = 100% покрытие операторов
Но подождите — что если есть баг, проявляющийся только когда is_member равно False? Покрытие операторов не обнаружит этого, потому что путь else не требовалось тестировать.
Ограничение: 100% покрытие операторов не означает 100% протестировано. Это лишь означает, что каждая строка была достигнута хотя бы раз.
Покрытие ветвей (Branch Coverage)
Покрытие ветвей (также называемое покрытием решений) измеряет, был ли протестирован каждый возможный результат каждой точки принятия решения. Каждый if, while, for, switch и тернарный оператор создают ветви.
Формула: Покрытие ветвей = (Выполненные ветви / Всего ветвей) × 100%
Используя ту же функцию:
def calculate_discount(price, is_member):
discount = 0
if is_member: # Ветвь: True / False
discount = price * 0.1
final_price = price - discount
return final_price
if is_member создаёт две ветви: True и False.
Тест 1: calculate_discount(100, True) → проверяет ветвь True
Тест 2: calculate_discount(100, False) → проверяет ветвь False
Оба теста вместе достигают 100% покрытия ветвей (2/2 ветви).
Покрытие ветвей сильнее покрытия операторов. 100% покрытие ветвей гарантирует 100% покрытие операторов, но не наоборот.
Покрытие путей (Path Coverage)
Покрытие путей требует тестирования каждого возможного пути выполнения через функцию. Путь — это уникальная последовательность операторов от входа до выхода.
Для функции с двумя независимыми if существуют 4 пути:
def process_order(is_member, has_coupon):
price = 100
if is_member: # Решение 1
price -= 10
if has_coupon: # Решение 2
price -= 5
return price
| Путь | is_member | has_coupon | Результат |
|---|---|---|---|
| 1 | True | True | 85 |
| 2 | True | False | 90 |
| 3 | False | True | 95 |
| 4 | False | False | 100 |
С циклами количество путей может стать бесконечным (цикл выполняется 0, 1, 2 раза, …), что делает 100% покрытие путей непрактичным для реального кода.
Тестирование потока данных
Тестирование потока данных (data flow testing) отслеживает, как значения данных проходят через код. Оно фокусируется на жизненном цикле переменных:
- Определение (def) — где переменной присваивается значение
- Использование (use) — где переменная считывается (в вычислении или решении)
- Уничтожение (kill) — где переменная становится неопределённой или выходит из области видимости
Распространённые критерии потока данных:
- All-defs: Каждое определение каждой переменной достигается хотя бы одним использованием
- All-uses: Каждая пара определение-использование покрыта
- All-du-paths: Каждый путь определение-использование покрыт
Тестирование потока данных эффективно для нахождения ошибок инициализации, неиспользуемых переменных и висячих ссылок.
Когда использовать White-Box Testing
White-box тестирование наиболее эффективно для:
- Юнит-тестирования — тестирование отдельных функций и методов
- Рефакторинга — гарантия сохранения поведения после структурных изменений
- Тестирования безопасности — поиск уязвимостей в аутентификации, шифровании, обработке входных данных
- Оптимизации — выявление мёртвого кода и недостижимых путей
- Регуляторного соответствия — отрасли авиации (DO-178C) и автомобилестроения (ISO 26262) требуют определённых уровней покрытия
Преимущества и ограничения
| Преимущества | Ограничения |
|---|---|
| Тестирует каждый путь кода, а не только требования | Не находит отсутствующую функциональность (тестирует только существующее) |
| Находит мёртвый код и недостижимые ветви | Требует доступа к коду и навыков программирования |
| Помогает оптимизировать код, выявляя сложность | Поддержка тестов дорога при частых изменениях кода |
| Автоматизируется инструментами покрытия | 100% покрытие не гарантирует отсутствие багов |
| Обнаруживает логические ошибки, невидимые для black-box | Не валидирует требования пользователя и юзабилити |
Инструменты покрытия по языкам
| Язык | Популярные инструменты |
|---|---|
| Java | JaCoCo, Cobertura, Emma |
| JavaScript/TypeScript | Istanbul/nyc, c8, Vitest coverage |
| Python | Coverage.py, pytest-cov |
| C# | dotCover, OpenCover |
| Go | Встроенный go test -cover |
| Ruby | SimpleCov |
Эти инструменты генерируют отчёты, показывающие, какие строки, ветви и функции были выполнены во время тестирования, часто с визуальной подсветкой в IDE.
Упражнение: Рассчитайте покрытие операторов и ветвей
Дана следующая функция:
def validate_password(password):
errors = [] # S1
if len(password) < 8: # S2, Ветвь B1 (T/F)
errors.append("Too short") # S3
if len(password) > 64: # S4, Ветвь B2 (T/F)
errors.append("Too long") # S5
has_upper = False # S6
has_digit = False # S7
for char in password: # S8, Ветвь B3 (вход/пропуск)
if char.isupper(): # S9, Ветвь B4 (T/F)
has_upper = True # S10
if char.isdigit(): # S11, Ветвь B5 (T/F)
has_digit = True # S12
if not has_upper: # S13, Ветвь B6 (T/F)
errors.append("No uppercase") # S14
if not has_digit: # S15, Ветвь B7 (T/F)
errors.append("No digit") # S16
return errors # S17
Часть 1: Вы запускаете следующие тест-кейсы:
- Тест A:
validate_password("Ab1cdefgh")— валидный пароль (8+ символов, есть заглавная, есть цифра) - Тест B:
validate_password("short")— слишком короткий, без заглавной, без цифры
Рассчитайте покрытие операторов и покрытие ветвей, достигнутое двумя тестами совместно.
Часть 2: Какие дополнительные тест-кейсы нужны для достижения 100% покрытия ветвей? Перечислите их и объясните, какие ветви они покрывают.
Часть 3: Достижимо ли 100% покрытие путей для этой функции? Объясните, почему да или нет, и оцените количество уникальных путей.
Подсказка
Для Части 1 пройдите каждый тест-кейс строка за строкой, отмечая выполненные операторы и результаты ветвей. Помните, что тело цикла `for` выполняется один раз на каждый символ.Для Части 2 найдите результаты ветвей (True или False), которые не были задействованы Тестами A и B.
Решение
Часть 1: Расчёт покрытия
Тест A: validate_password("Ab1cdefgh")
- S1: ✅, S2: ✅ → B1-False, S4: ✅ → B2-False
- S6: ✅, S7: ✅, S8: ✅ → B3-Вход
- S9: ✅ → B4-True (для ‘A’), B4-False (для остальных)
- S10: ✅, S11: ✅ → B5-True (для ‘1’), B5-False (для остальных)
- S12: ✅, S13: ✅ → B6-False, S15: ✅ → B7-False, S17: ✅
- Не выполнены: S3, S5, S14, S16
Тест B: validate_password("short")
- S1: ✅, S2: ✅ → B1-True, S3: ✅, S4: ✅ → B2-False
- S6: ✅, S7: ✅, S8: ✅ → B3-Вход
- S9: ✅ → B4-False, S11: ✅ → B5-False
- S13: ✅ → B6-True, S14: ✅, S15: ✅ → B7-True, S16: ✅, S17: ✅
Суммарное покрытие операторов: Выполнено: S1-S4, S6-S9, S10-S17 = 16 операторов Не выполнен: S5 Покрытие: 16/17 = 94.1%
Суммарное покрытие ветвей:
- B1: True ✅, False ✅
- B2: True ❌, False ✅
- B3: Вход ✅, Пропуск ❌
- B4: True ✅, False ✅
- B5: True ✅, False ✅
- B6: True ✅, False ✅
- B7: True ✅, False ✅
Покрыто: 11 из 14 Покрытие: 11/14 = 78.6%
Часть 2: Дополнительные тесты для 100% покрытия ветвей
Тест C: validate_password("") — покрывает B3-Пропуск (пустая строка, цикл не выполняется)
Тест D: validate_password("A" * 65) — покрывает B2-True (длина > 64)
С Тестами C и D: все 14 ветвей покрыты = 100% покрытие ветвей.
Часть 3: Достижимость покрытия путей
100% покрытие путей недостижимо, потому что:
- Цикл
forитерируется по каждому символу. Для пароля длиной N каждый символ может принять B4-True или B4-False И B5-True или B5-False, создавая 4 комбинации на символ. - Для пароля длиной 8: примерно 4^8 = 65,536 путей только через цикл.
- Это делает исчерпывающее тестирование путей непрактичным. На практике используется выбор путей на основе граничных значений.
Стандарты покрытия в отрасли
Разные отрасли требуют разные уровни покрытия:
- DO-178C (авиация): Уровень A (катастрофический отказ) требует MC/DC (Modified Condition/Decision Coverage), что строже покрытия ветвей
- ISO 26262 (автомобилестроение): ASIL D требует MC/DC; ASIL A требует покрытия операторов
- IEC 62304 (медицинские устройства): ПО класса C требует покрытия ветвей
- Общие веб/мобильные проекты: Большинство команд нацеливаются на 80% покрытия операторов как практичный базовый уровень
Ключевой вывод: покрытие — необходимое, но не достаточное условие качества. Высокое покрытие означает, что вы протестировали много кода. Это не означает, что вы протестировали его хорошо.
Ключевые выводы
- White-box тестирование проектирует тесты на основе внутренней структуры кода, требуя доступа к исходному коду
- Покрытие операторов обеспечивает выполнение каждой строки; покрытие ветвей — каждого результата решения; покрытие путей — каждого пути выполнения
- Критерии покрытия имеют иерархию включения: пути > ветви > операторы
- 100% покрытие не гарантирует отсутствие багов — это лишь означает, что код был достигнут
- White-box тестирование необходимо для юнит-тестирования, анализа безопасности и регуляторного соответствия
- Инструменты покрытия автоматизируют измерение и формирование отчётов для всех основных языков программирования