Честный разговор о TDD: где помогает, где замедляет. Прагматичный подход без фанатизма
TDD не серебряная пуля. Это инструмент, который работает в конкретных ситуациях. Честный разговор о том, когда использовать TDD, а когда нет.
# ✅ ОТЛИЧНО: Requirements понятны
"""
Requirement: Discount calculation
- Regular customer: 0% discount
- Gold customer (>10 orders): 10% discount
- Gold customer on Friday: 15% discount
"""
# TDD идеален — requirements → tests → code
def test_regular_customer_no_discount():
discount = calculate_discount("regular", orders=5, day="monday")
assert discount == 0.0
def test_gold_customer_discount():
discount = calculate_discount("gold", orders=15, day="monday")
assert discount == 0.10Почему работает: Requirements детерминированы → тесты очевидны.
# ✅ ОТЛИЧНО: Pure function без side effects
def test_format_phone_number():
assert format_phone("+14155552671") == "(415) 555-2671"
assert format_phone("4155552671") == "(415) 555-2671"
assert format_phone("415-555-2671") == "(415) 555-2671"
# Реализация
def format_phone(number: str) -> str:
digits = ''.join(c for c in number if c.isdigit())
return f"({digits[0:3]}) {digits[3:6]}-{digits[6:10]}"Почему работает: Нет зависимостей, детерминировано, легко тестировать.
# ✅ ОТЛИЧНО: API контракт должен быть стабильным
def test_create_user_api_contract():
"""TDD гарантирует контракт не сломается"""
response = client.post("/users", json={"email": "test@test.com"})
assert response.status_code == 201
assert "id" in response.json()
assert "email" in response.json()
assert "created_at" in response.json()Почему работает: Контракт критичен, TDD предотвращает breaking changes.
# ✅ ОТЛИЧНО: Много edge cases
@pytest.mark.parametrize("input,expected", [
("", False), # Empty
("a", False), # Single char
("aa", True), # Simple palindrome
("A man a plan a canal Panama", True), # Complex with spaces
("race a car", False), # Not palindrome
])
def test_is_palindrome(input, expected):
assert is_palindrome(input) == expectedПочему работает: Тесты документируют все edge cases.
# ❌ ПЛОХО: Вы не знаете как работает библиотека
import new_obscure_library
def test_library_does_something():
result = new_obscure_library.process(data)
assert result == ??? # Что проверять???Решение: Сначала explore (spike), потом тесты.
# ✅ ХОРОШО: Spike first
# 1. Experiment в REPL / Jupyter
result = new_obscure_library.process({"key": "value"})
print(result) # {'status': 'ok', 'processed': True}
# 2. Теперь тесты понятны
def test_library_process():
result = new_obscure_library.process({"key": "value"})
assert result["status"] == "ok"# ❌ ПЛОХО: TDD для UI бессмысленно
def test_button_looks_good():
button = render_button(color="blue", size="large")
assert button.color == "#0000FF" # Субъективно!
assert button.width == 200 # А если дизайн изменится?Решение: Visual regression tests (screenshot comparison) или manual testing.
# ❌ ПЛОХО: Тесты замедляют iteration
# Вы не знаете какой API нужен
class UserService:
def create_user(self, email): # Или user_data dict?
pass # Или validate_and_create? Или register?
# Тесты будут переписаны 10 раз за деньРешение: Сначала prototype API без тестов, затем стабилизируйте с тестами.
# ❌ ПЛОХО: Тесты для одноразового кода
def migrate_users_from_old_db():
"""Скрипт запустится один раз и больше никогда"""
# 100 строк миграционной логики
# Тесты бессмысленны — скрипт одноразовыйРешение: Запустите, проверьте вручную, удалите.
# 1. Начните с integration test (outside)
def test_create_todo_via_api():
response = client.post("/todos", json={"title": "Buy milk"})
assert response.status_code == 201
# 2. Затем unit tests (inside)
def test_todo_service_create():
service = TodoService(mock_repo)
todo = service.create("Buy milk")
assert todo.title == "Buy milk"Преимущества:
# 1. Exploratory spike (без тестов)
def process_payment(amount, card):
# Эксперименты с Stripe API
charge = stripe.Charge.create(amount=amount, source=card)
return charge
# 2. После понимания → TDD refactoring
def test_process_payment_success():
mock_stripe = Mock()
mock_stripe.Charge.create.return_value = Mock(id="ch_123")
result = process_payment_v2(100, "tok_visa", stripe_client=mock_stripe)
assert result["charge_id"] == "ch_123"# ✅ TDD для critical business logic
def test_calculate_invoice_total():
invoice = Invoice(items=[...], tax_rate=0.1, discount=0.05)
assert invoice.total() == 105.0 # Критично для бизнеса!
# ❌ NO TDD для вспомогательных утилит
def format_invoice_date(date):
return date.strftime("%Y-%m-%d") # Некритично, тест после# Проект Todo App (100% code)
# 40% — TDD (написано test-first)
# - Business logic (subscriptions, payments)
# - API contracts (REST endpoints)
# - Critical utilities (discount calculation)
# 30% — Test-after (написано code-first)
# - Database models (знали структуру заранее)
# - Simple CRUD operations
# - UI components (после sketch)
# 20% — No tests
# - One-time migration scripts
# - Debug utilities
# - Local development tools
# 10% — Manual testing
# - UI appearance
# - Email templates
# - Admin dashboard# Используйте TDD если:
complexity = "high" # Много условий / edge cases
requirements = "clear" # Знаете что делать
change_frequency = "high" # Часто меняется
criticality = "high" # Баги дорого обходятся
# НЕ используйте TDD если:
complexity = "low" # Простая логика
requirements = "unclear" # Не знаете что нужно
change_frequency = "low" # Редко меняется
prototype = True # Эксперимент / spike# ❌ Фанатизм
"100% TDD всегда и везде!" # Медленно и неэффективно
# ✅ Прагматизм
"""
TDD для:
- Critical business logic
- Public APIs
- Complex algorithms
Code-first для:
- Exploration / spikes
- Simple CRUD
- UI prototypes
No tests для:
- One-time scripts
- Debug tools
"""Итог: TDD — мощный инструмент, но не догма. Используйте там, где он даёт ценность.