TL;DR: Тестирование белого ящика исследует внутреннюю структуру кода через метрики покрытия: операторов, ветвей, путей и условий. Требует знания программирования и доступа к исходникам. Лучше всего подходит для юнит-тестов, тестирования безопасности и валидации алгоритмов. Цель: 80%+ покрытие операторов, 70%+ покрытие ветвей.
Тестирование белого ящика — это техника верификации кода, при которой тестировщики и разработчики изучают внутреннюю структуру, логику и алгоритмы приложения для проектирования полноценных тест-кейсов. В отличие от тестирования чёрного ящика, которое фокусируется на входах и выходах, white box testing — или структурное тестирование — требует доступа к исходному коду и знания программирования. Согласно отчёту NIST 2023 года, 64% уязвимостей, обнаруженных в продакшене, могли бы быть найдены раньше при тщательном структурном тестировании на этапе разработки. По данным Google Site Reliability Engineering, команды с покрытием ветвей более 70% сталкиваются с 40% меньше инцидентов в продакшене, связанных с логическими ошибками. Основные техники: покрытие операторов, покрытие ветвей, покрытие путей и анализ потока данных — каждая выявляет разные категории дефектов.
Что такое Тестирование Белого Ящика?
Тестирование белого ящика анализирует исходный код для проектирования тест-кейсов на конкретные пути выполнения. Требует доступа к коду и навыков программирования.
Тестирование белого ящика включает анализ исходного кода для разработки тестовых случаев, которые выполняют определённые пути кода, условия и логику. Тестировщикам нужны знания программирования и доступ к исходному коду для эффективного выполнения тестирования белого ящика.
“Тестирование белого ящика — это не просто проценты покрытия. Это поиск скрытых путей в логике, которые ни один пользователь не догадается протестировать, но которые способны обрушить продакшн в самый неподходящий момент.” — Юрий Кан, Senior QA Lead
Ключевые Характеристики
- Видимость кода: Полный доступ к исходному коду и внутренней структуре
- Структурный фокус: Тестирует внутреннюю логику, алгоритмы и пути кода
- Ориентация на разработчиков: Часто выполняется разработчиками или техническими тестировщиками
- Управляемость покрытием: Нацелен на высокие метрики покрытия кода
- Раннее обнаружение дефектов: Находит баги на этапе разработки
Когда Использовать Тестирование Белого Ящика
Тестирование белого ящика идеально для:
- Модульного тестирования: Тестирование отдельных функций и методов
- Интеграционного тестирования: Проверка взаимодействий компонентов
- Тестирования безопасности: Поиск уязвимостей в логике кода
- Оптимизации кода: Выявление узких мест производительности
- Валидации алгоритмов: Обеспечение правильности вычислений
Метрики Покрытия Кода
1. Покрытие Операторов
Покрытие операторов измеряет процент исполняемых операторов, выполненных тестами. Каждая строка кода должна выполниться хотя бы один раз.
Формула: Покрытие Операторов = (Выполненные Операторы / Всего Операторов) × 100%
Пример:
def calculate_discount(price, is_member, purchase_count):
"""Рассчитать скидку на основе членства и истории покупок"""
discount = 0 # Оператор 1
if is_member: # Оператор 2
discount = 10 # Оператор 3
if purchase_count > 5: # Оператор 4
discount += 5 # Оператор 5
return discount # Оператор 6
# Тестовый случай 1: Не член, мало покупок
def test_no_discount():
result = calculate_discount(100, False, 2)
assert result == 0
# Покрытие: Операторы 1, 2, 4, 6 = 4/6 = 67%
# Тестовый случай 2: Член с многими покупками
def test_full_discount():
result = calculate_discount(100, True, 10)
assert result == 15
# Покрытие: Все операторы = 6/6 = 100%
Ограничения: 100% покрытие операторов не гарантирует, что все сценарии протестированы—только то, что все строки выполнились.
2. Покрытие Ветвей (Покрытие Решений)
Покрытие ветвей обеспечивает, что каждая точка принятия решения (if/else, switch, циклы) оценивается как истина и ложь. Это сильнее, чем покрытие операторов.
Формула: Покрытие Ветвей = (Выполненные Ветви / Всего Ветвей) × 100%
Пример:
def validate_password(password):
"""Валидировать надёжность пароля"""
if len(password) < 8: # Решение 1
return "Слишком короткий"
if not any(c.isupper() for c in password): # Решение 2
return "Нужны заглавные буквы"
if not any(c.isdigit() for c in password): # Решение 3
return "Нужна цифра"
return "Валидный"
# Ветви:
# Решение 1: Истина, Ложь
# Решение 2: Истина, Ложь
# Решение 3: Истина, Ложь
# Всего: 6 ветвей
def test_branch_coverage():
# Тест 1: Покрывает Р1=Истина
assert validate_password("короткий") == "Слишком короткий"
# Тест 2: Покрывает Р1=Ложь, Р2=Истина
assert validate_password("строчные123") == "Нужны заглавные буквы"
# Тест 3: Покрывает Р1=Ложь, Р2=Ложь, Р3=Истина
assert validate_password("БезЦифр") == "Нужна цифра"
# Тест 4: Покрывает Р1=Ложь, Р2=Ложь, Р3=Ложь
assert validate_password("Валидный123") == "Валидный"
# Покрытие Ветвей: 6/6 = 100%
3. Покрытие Путей
Покрытие путей тестирует все возможные пути через код. Для кода с множественными решениями количество путей растёт экспоненциально.
Пример:
def process_order(item_count, is_premium, in_stock):
"""Обработать заказ с множественными условиями"""
message = ""
if item_count > 0: # Решение A
if in_stock: # Решение B
message = "Обработка"
if is_premium: # Решение C
message += " с приоритетом"
else:
message = "Нет в наличии"
else:
message = "Неверное количество"
return message
# Возможные пути:
# Путь 1: A=Ложь → "Неверное количество"
# Путь 2: A=Истина, B=Ложь → "Нет в наличии"
# Путь 3: A=Истина, B=Истина, C=Ложь → "Обработка"
# Путь 4: A=Истина, B=Истина, C=Истина → "Обработка с приоритетом"
def test_all_paths():
# Путь 1
assert process_order(0, False, True) == "Неверное количество"
# Путь 2
assert process_order(5, False, False) == "Нет в наличии"
# Путь 3
assert process_order(5, False, True) == "Обработка"
# Путь 4
assert process_order(5, True, True) == "Обработка с приоритетом"
# Покрытие Путей: 4/4 = 100%
Вызов: Для n независимых решений существует 2^n возможных путей. Покрытие путей становится непрактичным для сложного кода.
4. Покрытие Условий
Покрытие условий обеспечивает, что каждое булево подвыражение оценивается как истина и ложь независимо.
Пример:
def can_access(is_authenticated, has_permission, is_active):
"""Проверить, может ли пользователь получить доступ к ресурсу"""
# Составное условие с 3 булевыми подвыражениями
if is_authenticated and has_permission and is_active:
return "Доступ предоставлен"
return "Доступ запрещён"
# Для 100% покрытия условий каждая переменная должна быть Истиной и Ложной:
def test_condition_coverage():
# is_authenticated: Истина
can_access(True, True, True)
# is_authenticated: Ложь
can_access(False, True, True)
# has_permission: Истина (уже покрыто выше)
# has_permission: Ложь
can_access(True, False, True)
# is_active: Истина (уже покрыто)
# is_active: Ложь
can_access(True, True, False)
# Покрытие Условий: 100%
5. Покрытие Множественных Условий (ПМУ)
Покрытие множественных условий тестирует все комбинации булевых подвыражений.
Пример:
def authorize_action(is_admin, is_owner):
"""Авторизовать действие на основе роли и владения"""
if is_admin or is_owner:
return True
return False
# Таблица истинности для 'is_admin or is_owner':
# is_admin | is_owner | Результат
# И | И | И
# И | Л | И
# Л | И | И
# Л | Л | Л
def test_mcc():
assert authorize_action(True, True) == True # И, И
assert authorize_action(True, False) == True # И, Л
assert authorize_action(False, True) == True # Л, И
assert authorize_action(False, False) == False # Л, Л
# ПМУ: 4/4 = 100%
Техники Тестирования Белого Ящика
1. Тестирование Потока Управления
Тестирование потока управления визуализирует код как граф, где узлы — это операторы, а рёбра — это пути потока управления.
Пример: Граф потока для валидации входа в систему
def validate_login(username, password):
# Узел 1: Вход
if not username: # Узел 2
return "Требуется имя пользователя" # Узел 3
if len(password) < 8: # Узел 4
return "Пароль слишком короткий" # Узел 5
user = find_user(username) # Узел 6
if user and check_password(user, password): # Узел 7
return "Успех" # Узел 8
return "Неверные учётные данные" # Узел 9
# Граф потока:
# 1
# ↓
# 2 → 3 (возврат)
# ↓
# 4 → 5 (возврат)
# ↓
# 6
# ↓
# 7 → 8 (возврат)
# ↓
# 9 (возврат)
# Цикломатическая Сложность = Р - У + 2С
# Р=рёбра, У=узлы, С=связанные компоненты
# Сложность = 3 (3 точки принятия решений + 1)
2. Тестирование Потока Данных
Тестирование потока данных отслеживает определения и использования переменных для обеспечения правильной обработки данных.
Пример:
def calculate_total(items):
"""Рассчитать общую цену с логикой скидки"""
total = 0 # Определение 'total'
for item in items:
total += item.price # Использование 'total', затем переопределение
discount = 0 # Определение 'discount'
if total > 100: # Использование 'total'
discount = total * 0.1 # Переопределение 'discount'
final = total - discount # Использование 'total' и 'discount'
return final
# Аномалии потока данных для тестирования:
# - Определена, но никогда не использована (dd)
# - Использована, но никогда не определена (ur)
# - Определена дважды без использования между ними (dd)
def test_data_flow():
# Тестировать жизненный цикл переменных
items = [Item(50), Item(60)] # total=110, discount=11
assert calculate_total(items) == 99
items = [Item(30), Item(40)] # total=70, discount=0
assert calculate_total(items) == 70
3. Тестирование Циклов
Тестирование циклов проверяет поведение циклов на границах и во время выполнения.
Стратегии тестирования циклов:
def process_batch(items, max_retries=3):
"""Обработать элементы с логикой повторных попыток"""
processed = []
for item in items: # Цикл 1
retry_count = 0
while retry_count < max_retries: # Цикл 2 (вложенный)
if process_item(item):
processed.append(item)
break
retry_count += 1
else:
# Выполняется, если цикл завершается без break
log_failure(item)
return processed
def test_loops():
# Простые тесты циклов:
# 1. Пропустить цикл (0 итераций)
assert len(process_batch([])) == 0
# 2. Одна итерация
assert len(process_batch([Item(1)])) == 1
# 3. Две итерации
assert len(process_batch([Item(1), Item(2)])) == 2
# 4. Максимум итераций
items = [Item(i) for i in range(100)]
assert len(process_batch(items)) <= 100
# Тесты вложенных циклов:
# 5. Внутренний цикл выполняется максимальное количество раз
failing_item = FailingItem()
result = process_batch([failing_item])
# Проверить, что retry_count достиг max_retries
4. Мутационное Тестирование
Мутационное тестирование вносит небольшие изменения кода (мутации) для проверки, что тесты могут их обнаружить.
Пример:
# Оригинальный код
def is_eligible(age, has_license):
return age >= 18 and has_license
# Мутация 1: Изменить >= на >
def is_eligible_mutant1(age, has_license):
return age > 18 and has_license # Мутант
# Мутация 2: Изменить 'and' на 'or'
def is_eligible_mutant2(age, has_license):
return age >= 18 or has_license # Мутант
# Тесты должны убивать мутации
def test_eligibility():
# Этот тест убивает Мутацию 1 (age=18 должно пройти, но не проходит в мутанте)
assert is_eligible(18, True) == True
# Этот тест убивает Мутацию 2 (должно не пройти без лицензии)
assert is_eligible(20, False) == False
# Оценка Мутаций = Убитые Мутации / Всего Мутаций
# Оценка = 2/2 = 100%
Инструменты Тестирования Белого Ящика
Инструменты Статического Анализа
# Пример: Использование pylint для статического анализа
# Запуск: pylint my_module.py
def calculate_interest(principal, rate, time):
"""Рассчитать простой процент"""
result = principal * rate * time / 100
return result
# Pylint проверяет:
# - Неиспользуемые переменные
# - Неопределённые переменные
# - Несоответствия типов
# - Code smells
# - Метрики сложности
Инструменты Покрытия Кода
# Использование pytest-cov для отчёта о покрытии
# Запуск: pytest --cov=myapp --cov-report=html
# пример отчёта о покрытии:
"""
Name Stmts Miss Cover
-------------------------------------------
myapp/auth.py 45 2 96%
myapp/orders.py 67 8 88%
myapp/payment.py 34 0 100%
-------------------------------------------
TOTAL 146 10 93%
"""
# Конфигурация .coveragerc
"""
[run]
source = myapp
omit = */tests/*
[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
"""
Инструменты Профилирования
import cProfile
import pstats
def analyze_performance():
"""Профилировать выполнение кода"""
profiler = cProfile.Profile()
profiler.enable()
# Код для профилирования
result = expensive_operation()
profiler.disable()
stats = pstats.Stats(profiler)
stats.sort_stats('cumulative')
stats.print_stats()
# Вывод показывает:
# - Вызовы функций
# - Время на функцию
# - Счётчики вызовов
# - Горячие точки для оптимизации
Лучшие Практики Тестирования Белого Ящика
1. Нацеливаться на Высокое Покрытие, Не 100%
# Фокусироваться на критических путях, а не на полном покрытии
class PaymentProcessor:
def process_payment(self, amount, method):
"""Критический путь - должен иметь 100% покрытие"""
if amount <= 0:
raise ValueError("Недопустимая сумма")
if method == "credit_card":
return self._process_credit_card(amount)
elif method == "paypal":
return self._process_paypal(amount)
else:
raise ValueError("Неизвестный метод")
def format_receipt(self, transaction):
"""Низкий риск - 80% покрытие приемлемо"""
# Менее критическая логика форматирования
pass
# Приоритизировать покрытие для:
# - Обработка платежей: 100%
# - Функции безопасности: 100%
# - Бизнес-логика: 95%+
# - Форматирование UI: 70-80%
2. Тестировать Крайние Случаи и Границы
def get_age_category(age):
"""Категоризировать возрастные группы"""
if age < 0:
raise ValueError("Возраст не может быть отрицательным")
elif age < 13:
return "ребёнок"
elif age < 20:
return "подросток"
elif age < 65:
return "взрослый"
else:
return "пожилой"
def test_edge_cases():
# Неверный ввод
with pytest.raises(ValueError):
get_age_category(-1)
# Границы
assert get_age_category(0) == "ребёнок"
assert get_age_category(12) == "ребёнок"
assert get_age_category(13) == "подросток"
assert get_age_category(19) == "подросток"
assert get_age_category(20) == "взрослый"
assert get_age_category(64) == "взрослый"
assert get_age_category(65) == "пожилой"
assert get_age_category(100) == "пожилой"
3. Использовать Мокирование для Зависимостей
from unittest.mock import Mock, patch
class UserService:
def __init__(self, db, email_service):
self.db = db
self.email_service = email_service
def register_user(self, email, password):
"""Зарегистрировать нового пользователя"""
if self.db.user_exists(email):
return False
user = self.db.create_user(email, password)
self.email_service.send_welcome(user)
return True
def test_registration_with_mocks():
# Мокировать зависимости
mock_db = Mock()
mock_email = Mock()
# Настроить поведение мока
mock_db.user_exists.return_value = False
mock_db.create_user.return_value = {"id": 1, "email": "test@example.com"}
# Тест
service = UserService(mock_db, mock_email)
result = service.register_user("test@example.com", "password123")
# Проверить
assert result == True
mock_db.user_exists.assert_called_once_with("test@example.com")
mock_db.create_user.assert_called_once()
mock_email.send_welcome.assert_called_once()
4. Тестировать Обработку Ошибок
def divide_numbers(a, b):
"""Разделить два числа с обработкой ошибок"""
try:
result = a / b
return {"success": True, "result": result}
except ZeroDivisionError:
return {"success": False, "error": "Деление на ноль"}
except TypeError:
return {"success": False, "error": "Неверные типы"}
def test_error_handling():
# Счастливый путь
result = divide_numbers(10, 2)
assert result["success"] == True
assert result["result"] == 5
# Путь ZeroDivisionError
result = divide_numbers(10, 0)
assert result["success"] == False
assert "ноль" in result["error"]
# Путь TypeError
result = divide_numbers("10", "2")
assert result["success"] == False
assert "типы" in result["error"]
Реальный Пример Тестирования Белого Ящика
Тестирование Системы Корзины Покупок
class ShoppingCart:
def __init__(self):
self.items = []
self.discount_code = None
def add_item(self, product, quantity):
"""Добавить товар в корзину"""
if quantity <= 0:
raise ValueError("Количество должно быть положительным")
# Проверить, существует ли товар уже
for item in self.items:
if item['product'].id == product.id:
item['quantity'] += quantity
return
self.items.append({
'product': product,
'quantity': quantity
})
def calculate_total(self):
"""Рассчитать общую сумму корзины со скидками"""
subtotal = sum(
item['product'].price * item['quantity']
for item in self.items
)
discount = 0
if self.discount_code:
if self.discount_code.is_valid():
if self.discount_code.type == "percentage":
discount = subtotal * (self.discount_code.value / 100)
elif self.discount_code.type == "fixed":
discount = min(self.discount_code.value, subtotal)
return max(subtotal - discount, 0)
# Всесторонние тесты белого ящика
class TestShoppingCart:
def test_add_item_new_product(self):
"""Тест добавления нового товара (путь: товара нет в корзине)"""
cart = ShoppingCart()
product = Product(id=1, price=10)
cart.add_item(product, 2)
assert len(cart.items) == 1
assert cart.items[0]['quantity'] == 2
def test_add_item_existing_product(self):
"""Тест добавления существующего товара (путь: товар в корзине)"""
cart = ShoppingCart()
product = Product(id=1, price=10)
cart.add_item(product, 2)
cart.add_item(product, 3)
assert len(cart.items) == 1
assert cart.items[0]['quantity'] == 5
def test_add_item_invalid_quantity(self):
"""Тест обработки ошибок для неверного количества"""
cart = ShoppingCart()
product = Product(id=1, price=10)
with pytest.raises(ValueError):
cart.add_item(product, 0)
with pytest.raises(ValueError):
cart.add_item(product, -1)
def test_calculate_total_no_discount(self):
"""Тест расчёта общей суммы без скидки"""
cart = ShoppingCart()
cart.add_item(Product(id=1, price=10), 2)
cart.add_item(Product(id=2, price=15), 1)
assert cart.calculate_total() == 35
def test_calculate_total_percentage_discount(self):
"""Тест пути процентной скидки"""
cart = ShoppingCart()
cart.add_item(Product(id=1, price=100), 1)
cart.discount_code = DiscountCode(type="percentage", value=10, valid=True)
assert cart.calculate_total() == 90
def test_calculate_total_fixed_discount(self):
"""Тест пути фиксированной скидки"""
cart = ShoppingCart()
cart.add_item(Product(id=1, price=100), 1)
cart.discount_code = DiscountCode(type="fixed", value=20, valid=True)
assert cart.calculate_total() == 80
def test_calculate_total_fixed_discount_exceeds_subtotal(self):
"""Тест, что фиксированная скидка не уходит в минус"""
cart = ShoppingCart()
cart.add_item(Product(id=1, price=10), 1)
cart.discount_code = DiscountCode(type="fixed", value=50, valid=True)
assert cart.calculate_total() == 0
def test_calculate_total_invalid_discount_code(self):
"""Тест пути невалидного кода скидки"""
cart = ShoppingCart()
cart.add_item(Product(id=1, price=100), 1)
cart.discount_code = DiscountCode(type="percentage", value=10, valid=False)
assert cart.calculate_total() == 100
Анализ Отчёта о Покрытии
# Запустить покрытие
$ pytest --cov=shopping_cart --cov-report=term-missing
Name Stmts Miss Branch BrPart Cover Missing
---------------------------------------------------------------
shopping_cart.py 45 0 18 0 100%
---------------------------------------------------------------
TOTAL 45 0 18 0 100%
# Детали покрытия ветвей:
# - add_item: 4/4 ветви (100%)
# - calculate_total: 14/14 ветвей (100%)
Преимущества и Ограничения
Преимущества
- Раннее обнаружение багов: Находит дефекты во время разработки
- Оптимизация кода: Выявляет неэффективности и мёртвый код
- Полное покрытие: Может систематически тестировать все пути кода
- Валидация алгоритмов: Проверяет сложные вычисления
- Безопасность: Выявляет уязвимости в логике
Ограничения
- Требуется доступ к коду: Не подходит для сторонних систем
- Знание программирования: Тестировщикам нужны технические навыки
- Трудоёмкость: Тщательное тестирование требует значительных усилий
- Накладные расходы на обслуживание: Тесты ломаются при изменении кода
- Не валидирует требования: Может пропустить функциональные пробелы
Заключение
Тестирование белого ящика обеспечивает глубокое понимание качества кода путём исследования внутренних структур и логики. Через техники, такие как покрытие операторов, покрытие ветвей, покрытие путей и анализ потока данных, вы можете систематически проверять, что код ведёт себя правильно при всех условиях.
Ключ к эффективному тестированию белого ящика — баланс между целями покрытия и практическими ограничениями. Нацеливайтесь на высокое покрытие критических путей, используйте соответствующие инструменты для измерения и отслеживания покрытия, и комбинируйте техники белого ящика с тестированием чёрного ящика для всестороннего обеспечения качества.
Независимо от того, пишете ли вы модульные тесты для отдельных функций или анализируете сложные алгоритмы, тестирование белого ящика обеспечивает надёжность, эффективность и поддерживаемость вашего кода.
Официальные ресурсы
Структурное тестирование ловит дефекты рано: отчёт NIST 2023 по безопасности ПО показывает, что 64% уязвимостей из продакшена можно было найти раньше при тщательном структурном тестировании.
Согласно практикам Google Site Reliability Engineering, команды с покрытием ветвей 70%+ сталкиваются с 40% меньше инцидентов в продакшене из-за логических ошибок.
FAQ
Что такое тестирование белого ящика?
White box testing исследует внутреннюю структуру кода и логику. Иначе — структурное тестирование: требует доступа к коду и знания программирования.
Тестирование белого ящика исследует внутреннюю структуру кода, логику и алгоритмы приложения. Также называется структурным или стеклянным тестированием. Требует знания программирования и доступа к исходному коду для проектирования тест-кейсов, покрывающих конкретные пути выполнения.
В чём разница между белым и чёрным ящиком?
Белый ящик: внутренний код, доступ к исходникам, ориентация на покрытие. Чёрный ящик: входы/выходы, без кода, ориентация на поведение пользователя.
Белый ящик тестирует внутреннюю структуру кода и требует доступа к исходникам. Чёрный ящик рассматривает систему как непрозрачный блок, фокусируясь на входах/выходах. Белый ящик ориентирован на разработчиков и покрытие; чёрный ящик — на поведение с точки зрения пользователя.
Какие метрики покрытия важнее всего?
Покрытие операторов (80%+), покрытие ветвей (70%+), покрытие путей и покрытие условий — четыре ключевые метрики white box тестирования.
Покрытие операторов (цель 80%+) — исполненные строки. Покрытие ветвей (цель 70%+) — все исходы решений. Покрытие путей — все маршруты выполнения. Покрытие условий — каждое булево подвыражение в обоих значениях.
Когда использовать тестирование белого ящика?
Применяй для: юнит-тестирования функций, тестирования безопасности для поиска уязвимостей, валидации критичных алгоритмов, профилирования производительности и верификации бизнес-логики при код-ревью.
See Also
- Тестирование Серого Ящика: Лучшее из Двух Миров - Лучшее из двух миров: когда применять серый ящик, преимущества,…
- Тестирование Чёрного Ящика: Техники и Подходы - Освойте тестирование чёрного ящика: таблицы решений, переходы…
- Критерии входа и выхода в тестировании: когда начинать и заканчивать тестирование - Освойте критерии входа и выхода для определения четких границ фаз…
- Static Testing: находим дефекты без запуска кода - Изучите техники статического тестирования, включая ревью,…
- Модульное тестирование: принципы и лучшие практики - Принципы написания эффективных юнит-тестов для стабильного кода
