Как тестировать код, который не понимаешь. Документируем поведение через тесты. Фундамент работы с legacy
Legacy-код — это код без тестов, который страшно менять. Characterization Tests документируют текущее поведение кода через тесты.
Цель: Научиться покрывать тестами код, который вы не понимаете, чтобы безопасно его менять.
# legacy/discount.py
def calculate_discount(price, customer_type, items_count, day_of_week):
"""Рассчитать скидку (никто не знает как работает)"""
if customer_type == "gold":
discount = 0.2
if items_count > 10:
discount = 0.25
if day_of_week == "friday":
discount = 0.3
elif customer_type == "silver":
discount = 0.1
if items_count > 5:
discount = 0.15
else:
discount = 0
if items_count > 20:
discount = 0.05
return price * (1 - discount)Проблемы:
Characterization Test — тест, который документирует фактическое поведение кода, даже если вы не понимаете почему оно такое.
Цель: Зафиксировать текущее поведение, чтобы потом безопасно рефакторить.
# tests/test_discount_characterization.py
import pytest
from legacy.discount import calculate_discount
# Захватываем ВСЕ возможные комбинации
@pytest.mark.parametrize("price,customer_type,items,day,expected", [
# Gold customer
(100, "gold", 5, "monday", 80.0), # 20% discount
(100, "gold", 15, "monday", 75.0), # 25% discount (>10 items)
(100, "gold", 5, "friday", 70.0), # 30% discount (friday)
(100, "gold", 15, "friday", 70.0), # 30% discount (friday wins)
# Silver customer
(100, "silver", 3, "monday", 90.0), # 10% discount
(100, "silver", 7, "monday", 85.0), # 15% discount (>5 items)
(100, "silver", 7, "friday", 85.0), # Friday не влияет
# Regular customer
(100, "regular", 10, "monday", 100.0), # 0% discount
(100, "regular", 25, "monday", 95.0), # 5% discount (>20 items)
])
def test_discount_characterization(price, customer_type, items, day, expected):
"""Characterization test: документируем текущее поведение"""
result = calculate_discount(price, customer_type, items, day)
assert result == expectedpytest tests/test_discount_characterization.py -v
# Результат:
# tests/test_discount_characterization.py::test_discount_characterization[100-gold-5-monday-80.0] PASSED
# tests/test_discount_characterization.py::test_discount_characterization[100-gold-15-monday-75.0] PASSED
# ...
# ✅ Все тесты зелёные — поведение зафиксировано!# legacy/discount.py (REFACTORED)
def calculate_discount(price, customer_type, items_count, day_of_week):
"""Рассчитать скидку (рефакторинг)"""
discount_rules = {
"gold": {"base": 0.2, "bulk": (10, 0.25), "friday": 0.3},
"silver": {"base": 0.1, "bulk": (5, 0.15)},
"regular": {"base": 0.0, "bulk": (20, 0.05)},
}
rules = discount_rules.get(customer_type, {"base": 0.0})
# Friday скидка для gold
if customer_type == "gold" and day_of_week == "friday":
discount = rules["friday"]
# Bulk скидка
elif "bulk" in rules and items_count > rules["bulk"][0]:
discount = rules["bulk"][1]
else:
discount = rules["base"]
return price * (1 - discount)Запускаем тесты снова:
pytest tests/test_discount_characterization.py -v
# ✅ Все тесты зелёные — рефакторинг безопасный!# legacy/report.py
def generate_report(transactions):
"""Генерирует отчёт (сложная логика форматирования)"""
report = []
total = sum(t["amount"] for t in transactions)
report.append(f"Total Transactions: {len(transactions)}")
report.append(f"Total Amount: ${total:.2f}")
for t in transactions:
report.append(f"{t['date']} - {t['description']}: ${t['amount']:.2f}")
return "\n".join(report)# tests/test_report_golden.py
import pytest
from legacy.report import generate_report
def test_generate_report_golden_master():
"""Golden master: сравнение с эталонным выводом"""
transactions = [
{"date": "2025-01-01", "description": "Payment", "amount": 100.0},
{"date": "2025-01-02", "description": "Refund", "amount": -20.0},
]
result = generate_report(transactions)
# Эталонный вывод (captured from current code)
expected = """Total Transactions: 2
Total Amount: $80.00
2025-01-01 - Payment: $100.00
2025-01-02 - Refund: $-20.00"""
assert result == expectedАльтернатива: snapshot testing
# tests/test_report_snapshot.py
def test_generate_report_snapshot(snapshot):
"""Snapshot test: автоматическое сохранение вывода"""
transactions = [
{"date": "2025-01-01", "description": "Payment", "amount": 100.0},
]
result = generate_report(transactions)
# Первый запуск: pytest сохранит вывод в snapshots/
# Последующие запуски: сравнение с сохранённым
snapshot.assert_match(result, "report_output.txt")pip install approvaltests# tests/test_discount_approval.py
from approvaltests import verify
from legacy.discount import calculate_discount
def test_discount_all_scenarios():
"""Approval test: захватить все сценарии"""
scenarios = []
for customer in ["gold", "silver", "regular"]:
for items in [1, 10, 25]:
for day in ["monday", "friday"]:
result = calculate_discount(100, customer, items, day)
scenarios.append(f"{customer} | {items} items | {day} | ${result:.2f}")
output = "\n".join(scenarios)
verify(output)Первый запуск:
pytest tests/test_discount_approval.py
# Откроется diff tool (meld, kdiff3, vimdiff)
# Вы проверяете вывод и approveПоследующие запуски:
# legacy/tax.py
def calculate_tax(income, country, dependents, tax_year):
"""Рассчитать налог (сложная логика)"""
if country == "US":
if income < 10000:
rate = 0.1
elif income < 40000:
rate = 0.12
else:
rate = 0.22
if dependents > 0:
deduction = dependents * 2000
if tax_year == 2024:
rate -= 0.01 # Tax cut 2024
elif country == "UK":
if income < 12500:
rate = 0
elif income < 50000:
rate = 0.2
else:
rate = 0.4
deduction = 0
return income * rate - deductionВаша задача:
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: Legacy Code: Seams и внедрение тестов