Цикл TDD пошагово: тест → реализация → рефакторинг. Когда писать тест первым, когда после. Прагматичный подход
TDD (Test-Driven Development) — подход где тест пишется ДО реализации. Red-Green-Refactor — цикл из 3 шагов.
Цель: Понять TDD цикл и научиться применять прагматично (не фанатично).
1. RED: Напишите failing test
↓
2. GREEN: Минимальный код для прохождения теста
↓
3. REFACTOR: Улучшите код без изменения поведения
↓
Repeat
# tests/test_todo_priority.py
import pytest
from todo_app.models import TodoItem
def test_create_todo_with_priority():
"""RED: Тест который пока не проходит"""
todo = TodoItem(title="Buy milk", priority="high")
assert todo.title == "Buy milk"
assert todo.priority == "high"Запуск:
pytest tests/test_todo_priority.py
# ❌ FAILED: AttributeError: 'TodoItem' object has no attribute 'priority'✅ RED — тест падает (это правильно!)
# todo_app/models.py
class TodoItem:
def __init__(self, title, priority=None):
self.title = title
self.priority = priority # ← Минимальная реализацияЗапуск:
pytest tests/test_todo_priority.py
# ✅ PASSED✅ GREEN — тест проходит!
# todo_app/models.py
from enum import Enum
class Priority(Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class TodoItem:
def __init__(self, title: str, priority: Priority = Priority.MEDIUM):
self.title = title
self.priority = priority
def __repr__(self):
return f"TodoItem(title={self.title!r}, priority={self.priority.value})"Обновить тест:
def test_create_todo_with_priority():
"""Тест с Enum"""
todo = TodoItem(title="Buy milk", priority=Priority.HIGH)
assert todo.title == "Buy milk"
assert todo.priority == Priority.HIGHЗапуск:
pytest tests/test_todo_priority.py
# ✅ PASSED (рефакторинг не сломал тест)def test_sort_todos_by_priority():
"""RED: Сортировка ещё не реализована"""
todos = [
TodoItem("Task 1", Priority.LOW),
TodoItem("Task 2", Priority.HIGH),
TodoItem("Task 3", Priority.MEDIUM),
]
sorted_todos = sort_by_priority(todos)
assert sorted_todos[0].priority == Priority.HIGH
assert sorted_todos[1].priority == Priority.MEDIUM
assert sorted_todos[2].priority == Priority.LOW❌ NameError: name 'sort_by_priority' is not defined
# todo_app/sorting.py
def sort_by_priority(todos):
"""Минимальная реализация"""
priority_order = {Priority.HIGH: 0, Priority.MEDIUM: 1, Priority.LOW: 2}
return sorted(todos, key=lambda t: priority_order[t.priority])✅ PASSED
def test_sort_with_same_priority():
"""RED: Что если приоритет одинаковый?"""
todos = [
TodoItem("Z Task", Priority.HIGH),
TodoItem("A Task", Priority.HIGH),
]
sorted_todos = sort_by_priority(todos)
# Ожидаем алфавитную сортировку при одинаковом приоритете
assert sorted_todos[0].title == "A Task"
assert sorted_todos[1].title == "Z Task"❌ FAILED: Z Task идёт первым
def sort_by_priority(todos):
"""Сортировка с fallback на title"""
priority_order = {Priority.HIGH: 0, Priority.MEDIUM: 1, Priority.LOW: 2}
return sorted(todos, key=lambda t: (priority_order[t.priority], t.title))✅ PASSED
from dataclasses import dataclass, field
@dataclass(order=True)
class TodoItem:
sort_index: int = field(init=False, repr=False)
title: str
priority: Priority = Priority.MEDIUM
def __post_init__(self):
priority_order = {Priority.HIGH: 0, Priority.MEDIUM: 1, Priority.LOW: 2}
self.sort_index = (priority_order[self.priority], self.title)
# Теперь сортировка встроенная
def sort_by_priority(todos):
return sorted(todos)✅ Тесты проходят, код чище
# Вы не знаете как работает библиотека
import some_new_library
# ❌ Писать тест бессмысленно — вы не знаете что ожидать
def test_library_behavior():
result = some_new_library.do_something()
assert result == ??? # Что проверять?Решение: Сначала explore, потом тесты.
# ❌ TDD для цвета кнопки бессмысленно
def test_button_color():
button = render_button()
assert button.color == "#FF0000" # Проверка субъективнаРешение: Visual regression tests или manual testing.
# Одноразовый скрипт для миграции данных
def migrate_users():
# ...
# ❌ Тест не нужен — скрипт запустится один раз# ✅ TDD идеален для четкой бизнес-логики
def test_calculate_subscription_price():
# Тест определяет требования
price = calculate_subscription_price(
plan="premium",
users=5,
annual=True
)
assert price == 450 # $10/user/month * 12 months * 0.75 discount# ✅ TDD для pure functions
def test_format_currency():
assert format_currency(1234.56, "USD") == "$1,234.56"
assert format_currency(1234.56, "EUR") == "€1,234.56"# ✅ TDD для API гарантирует контракт
def test_create_user_api():
response = client.post("/users", json={"email": "test@test.com"})
assert response.status_code == 201
assert "id" in response.json()
assert response.json()["email"] == "test@test.com"# Гибридный подход
# 1. Начните с теста для happy path
def test_process_order_success():
result = process_order(user_id=1, item_id=10, quantity=2)
assert result["status"] == "success"
# 2. Напишите минимальный код
def process_order(user_id, item_id, quantity):
return {"status": "success"}
# 3. Добавьте тесты для edge cases БЕЗ TDD
def test_process_order_out_of_stock():
# Этот тест можно написать ПОСЛЕ реализации
result = process_order(user_id=1, item_id=999, quantity=2)
assert result["status"] == "error"
assert "out of stock" in result["message"]Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: TDD Практика: Premium-подписка (Часть 1)