Перейти к основному содержимому

White-box — тестирование потоков управления и данных

Разработчику Тестировщику

Зачем эта статья. Black-box отвечает на вопрос «что видит пользователь». White-box (структурное тестирование) отвечает на «какие ветки кода мы реально прошли». Для новичка в QA это мост к unit-тестам, покрытию и разговору с разработчиком на одном языке.

Black-box и white-box — в чём разница

ПодходЧто знаемТипичный исполнительПример
Black-boxТолько входы и выходы по ТЗРучной QA, API-тесты«Скидка 10 % при сумме > 1000» — проверили сумму 1500
White-boxСтруктура кода: if, циклы, присвоенияРазработчик (unit), иногда QA с доступом к кодуТот же сценарий + отдельный тест для ветки total ≤ 1000
Gray-boxЧастично знаем API/БД/логиИнтеграционные тестыUI + SQL + коды ошибок API

Black-box хватает для большинства API и UI. Внутри одного модуля может быть много веток if, и один «счастливый» сценарий не заходит в редкую, но опасную комбинацию (отрицательная скидка, деление на ноль, необработанный else).

White-box использует знание кода: граф потока управления, присвоения переменных, пути выполнения. Глава 2.3 учебника «экономика производства ПО» посвящена этому на уровне модулей и компонентов.

Кто пишет white-box тесты

Чаще разработчик (unit-тесты). QA с доступом к коду и покрытию подключается на критичных модулях: расчёты, безопасность, системы реального времени.


Граф потока управления (CFG)

Control Flow Graph (CFG) — схема выполнения программы:

  • Узел = базовый блок — последовательность команд без ветвлений внутри.
  • Ребро = переход после условия (if, while, return).

Пример:

def discount(total, is_vip): # блок 1 — вход
if total > 1000: # решение (ветвление)
rate = 0.1 if is_vip else 0.05 # блок 2 или 3
else:
rate = 0 # блок 4
return total * rate # блок 5 — выход

Для ветвевого покрытия (branch) нужны тесты, которые хотя бы раз прошли каждое ребро «да» и «нет» у решений. Минимум для примера выше:

#totalis_vipКакие ветки задействованы
11500truetotal>1000, vip
21500falsetotal>1000, обычный
3500falseelse, rate=0

Три теста — не «перебор ради перебора», а гарантия, что ветка else существует в прогоне.

Подробнее о метрике сложности — цикломатическая сложность.


Стратегии тестирования потока управления

СтратегияИдеяПокрытие
StatementКаждая строка выполнена хотя бы разСлабое: можно пройти строку без проверки результата
Branch / decisionКаждая ветка true/falseБазовое для модулей
ConditionВсе комбинации в сложном if a && bСильнее branch
Basis pathЧисло путей ≈ цикломатическая сложность MКлассика McCabe
Path (полное)Все путиЧасто нереально (экспоненциальный рост)

Basis path testing: выберите M линейно независимых путей — минимальный набор, чтобы разумно «пройти» ветвления. M считают по графу: число рёбер − узлов + 2 (для одной функции с одним входом).

:::info Сложность тестирования Чем выше M, тем больше обязательных сценариев. Функция с M > 15 — сигнал упростить код, а не «добить coverage любой ценой» (культура кода). :::

Связь с black-box техниками

Эквивалентные классы и границы говорят какие входы взять с точки зрения ТЗ. White-box показывает где в коде эти границы превращаются в if. Оба подхода дополняют друг друга.


Корректность white-box тестов

Тест корректен, если:

  1. Достижимость — путь реально выполняется (в тесте нет «мертвого» кода, который никогда не вызовется).
  2. Оракул — известен ожидаемый результат для входа (127).
  3. Независимость — тесты не зависят от порядка (FIRST в 120).
  4. Учёт данных — ветка пройдена с конкретными значениями, активирующими условие.

Ложное покрытие: тест «прошёл» по строке, но без assert на результат — coverage зелёный, дефект остаётся.

def discount(total, is_vip):
if total > 1000:
rate = 0.1 if is_vip else 0.05
else:
rate = 0
return total * rate

# Плохо: вызвали функцию, не проверили return
def test_discount_smoke():
discount(1500, True) # нет assert

# Хорошо
def test_vip_branch():
assert discount(1500, True) == 150.0

Тестирование с учётом переменных и констант

Условия зависят от границ:

КонструкцияЧто проверять
x > 1000999, 1000, 1001
a && bвсе 4 комбинации true/false
switch (code)каждый case + default
Константы #define TIMEOUT 50timeout−1, timeout, timeout+1

Потоки данных (data flow testing)

Поток данных — путь от определения (definition) переменной до использования (use).

АббревиатураСмысл
DEFПрисвоение значения
USEЧтение в выражении / условии
DU-chainЦепочка def → use

Классы покрытия (упрощённо):

КритерийТребование
All-defsКаждое определение доходит до хотя бы одного use в тесте
All-usesКаждое use покрыто тестом от какого-то def
All-du-pathsВсе пути def→use (дорого)

Мини-разбор

int x = ReadInput(); // DEF x
if (x < 0) x = 0; // DEF x (повторное)
return x * 2; // USE x
ТестВходОжиданиеКакой поток
1x = -5return 0clamp сработал
2x = 3return 6без clamp

Без теста с отрицательным x ветка clamp не проверена — statement coverage может быть обманчивым, если return вызывается только с положительными x из другого теста без отрицательных.


Связь control-flow и data-flow

Только control-flow+ data-flow
«Зашли в if»«Зашли с x=-1 и получили 0»
Может пропустить неинициализированную переменнуюЛовит USE без DEF

Статический анализ (Sonar, PVS-Studio) частично автоматизирует data-flow до запуска.


Инструменты

ИнструментЧто даёт
Coverage.py, JaCoCo, dotCoverStatement / branch coverage
pytest-cov, IstanbulОтчёты в CI
SonarQubeCoverage + smells
Отладчик / traceРеальный путь при падении

Цель — осознанное покрытие критичных модулей (126), а не зелёный процент ради отчёта.


White-box на уровне комплекса

На интеграции white-box смотрит на взаимодействие модулей: все ли интерфейсы вызваны, все ли коды ошибки обработаны. Это ближе к gray-box (111).

Для систем реального времени white-box дополняется измерением времени пути — см. заказные РВ-системы.


Когда QA без глубокого кода всё равно полезен white-box

СитуацияДействие
Разработчик говорит «покрытие 90 %»Спросить: branch или только statement? Есть ли assert?
Критичный расчёт (налог, скидка)Попросить таблицу граничных входов из кода
Падение только на prodС trace/log восстановить какая ветка не отработала
Рефакторинг ifРегресс + unit от разработчика по CFG

Типичные ошибки

  1. Гонка за 100% coverage — тесты без assert.
  2. Игнор else и catch — «так не бывает» в проде бывает.
  3. Не тестировать short-circuit — в a || expensive() вызов expensive() зависит от a.
  4. Путать branch с path — полное покрытие путей комбинаторно взрывается.

Куда читать дальше

ТемаМатериал
Unit-тесты120
Цикломатическая сложность7-10/2
Тест-дизайн black-box127
Маршрут курса7-13/intro

См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).