Что такое тестирование потоков данных?
Тестирование потоков данных фокусируется на жизненном цикле переменных: где они определяются (получают значение), где используются (читаются) и где уничтожаются (выходят из области видимости или переопределяются). Отслеживая эти события вдоль путей выполнения, тестирование потоков данных выявляет дефекты, которые другие техники пропускают.
Если тестирование потока управления спрашивает «какие пути проходит код?», то тестирование потоков данных спрашивает «что происходит с данными вдоль этих путей?»
Состояния переменных: Define, Use, Kill
Define (d): Переменная получает значение.
total = 0 # определение total
user = get_user() # определение user
Use (u): Значение переменной читается. Два типа:
- c-use (вычислительное использование): Значение в расчёте:
result = total * tax_rate - p-use (предикатное использование): Значение в условии:
if total > 100:
Kill (k): Переменная прекращает существование или переопределяется.
Аномалии потоков данных
Аномалия dd (define-define)
Переменная определяется дважды без использования между определениями.
price = get_base_price() # define
price = get_sale_price() # define снова — первое определение потеряно
discount = price * 0.1 # use
Аномалия ur (использование без определения)
Переменная используется до определения.
def calculate_total():
total = total + tax # БАГ: total используется до определения
return total
Аномалия du (определение без использования)
Переменная определена, но никогда не используется.
def process():
result = expensive_computation() # define
return "done" # result не используется
Пары Define-Use (DU-пары)
DU-пара — это пара (d, u), где:
- d — оператор, где переменная v определена
- u — оператор, где переменная v использована
- Существует путь от d к u без переопределения v (путь, свободный от определения)
Пример
def process_payment(amount, discount_code):
price = amount # Строка 1: define price
if discount_code == "SAVE10": # Строка 2: use discount_code (p-use)
discount = 0.10 # Строка 3: define discount
elif discount_code == "SAVE20": # Строка 4: use discount_code (p-use)
discount = 0.20 # Строка 5: define discount
else:
discount = 0 # Строка 6: define discount
final = price * (1 - discount) # Строка 7: use price, use discount (c-use)
return final # Строка 8: use final (c-use)
DU-пары для discount: (3, 7), (5, 7), (6, 7)
Критерии покрытия потоков данных
Покрытие All-Defs
Для каждого определения переменной хотя бы одна DU-пара покрыта.
Покрытие All-Uses
Для каждого определения каждое достижимое использование покрыто.
Покрытие All-DU-Paths
Для каждой DU-пары каждый свободный от определения путь между определением и использованием покрыт. Самый сильный критерий.
Типичные баги потоков данных
Null pointer из-за условного определения:
if condition:
connection = create_connection()
# БАГ: connection не определена, если condition = False
connection.execute(query)
Устаревшее значение после переприсвоения:
config = load_config("production")
if is_testing:
config = load_config("test")
setup_database(config)
config = load_config("production") # переопределение — зачем?
setup_cache(config) # всегда production config — баг при тестировании?
Упражнение: Анализ потоков данных
Задача 1
Определите все DU-пары и аномалии:
def calculate_grade(scores, curve):
total = 0 # Строка 1
count = 0 # Строка 2
average = 0 # Строка 3
for score in scores: # Строка 4
total = total + score # Строка 5
count = count + 1 # Строка 6
if count > 0: # Строка 7
average = total / count # Строка 8
average = average + curve # Строка 9
if average >= 90: # Строка 10
grade = "A" # Строка 11
elif average >= 80: # Строка 12
grade = "B" # Строка 13
else:
grade = "C" # Строка 14
return grade # Строка 15
Решение
Основные DU-пары:
total: (1, 5-use), (5, 5-use), (5, 8)count: (2, 6-use), (6, 6-use), (6, 7), (6, 8)average: (3, 9) если count==0, (8, 9) если count>0grade: (11, 15), (13, 15), (14, 15)
Аномалия: Строка 3 определяет average = 0. Если scores пуст, count=0, строка 8 пропускается. Строка 9 использует начальное average = 0, результат: grade = curve. Возможный логический баг.
Тест-кейсы для all-uses:
| # | scores | curve | Покрывает |
|---|---|---|---|
| 1 | [85, 95] | 5 | Цикл, count>0, average вычислен, >=90 |
| 2 | [70, 80] | 0 | Цикл, 80<=avg<90 |
| 3 | [50, 60] | 0 | Цикл, avg<80 |
| 4 | [] | 10 | Пустой scores, путь count=0 |
Задача 2
Найдите и исправьте баги потоков данных:
def process_order(items, coupon):
subtotal = 0
shipping = 0
for item in items:
subtotal += item.price * item.quantity
if subtotal > 50:
shipping = 0
if coupon:
discount = subtotal * coupon.percent / 100
total = subtotal - discount + shipping
return total
Решение
Баг 1: аномалия ur — discount используется до определения. Если coupon ложно, discount не определена. Исправление: инициализировать discount = 0.
Баг 2: аномалия dd — shipping всегда 0. Исправление: добавить стоимость доставки для маленьких заказов.
def process_order(items, coupon):
subtotal = 0
discount = 0
for item in items:
subtotal += item.price * item.quantity
if subtotal > 50:
shipping = 0
else:
shipping = 9.99
if coupon:
discount = subtotal * coupon.percent / 100
total = subtotal - discount + shipping
return total
Инструменты анализа потоков данных
- SonarQube — обнаруживает мёртвый код, неиспользуемые переменные, риски null pointer
- SpotBugs (Java) — находит неинициализированные чтения
- Pylint/Pyflakes (Python) — сообщает о неиспользуемых переменных
- ESLint (JavaScript) — правила no-unused-vars, no-undef
- Coverity — коммерческий инструмент с продвинутым анализом
Ключевые выводы
- Тестирование потоков данных отслеживает переменные через цикл define → use → kill
- DU-пары связывают определения с использованиями вдоль путей, свободных от определения
- Три уровня покрытия: all-defs (слабейший), all-uses, all-du-paths (сильнейший)
- Аномалии (dd, ur, du) часто указывают на реальные баги
- Самый частый баг: переменная определена только в одной ветви условия
- Инструменты статического анализа автоматизируют обнаружение аномалий
- Применяйте мышление потоков данных при code review даже без формальных инструментов