5. TDD на повышенной и пониженной передачах (стр. 109-119)
Пирамида тестирования
После введения сервисного слоя считаем тесты:
- 15 юнит-тестов (модель + сервисный слой)
- 8 интеграционных тестов (ORM + репозиторий)
- 2 сквозных теста (API)
Это правильная пирамида! Большинство тестов — быстрые юнит-тесты.
┌─────────────┐
│ E2E (2) │ ← Медленные, проверяют интеграцию
├─────────────┤
│Integration │
│ (8) │ ← Средние, с БД
├─────────────┤
│ Unit │
│ (15) │ ← Быстрые, в памяти
└─────────────┘
Повышенная и пониженная передачи
Метафора: как велосипед. Начинаем на пониженной передаче, чтобы тронуться. Затем переключаемся на повышенную для скорости. Если встречаем подъём — снова пониженная.
Пониженная передача — тесты модели
Когда использовать:
- Новый проект, сложная предметная область
- Нужно понять дизайн объектов
- Тестируем конкретную бизнес-логику
Преимущества:
- Точечный охват
- Высокая эффективность оценки
- Живая документация модели
Недостатки:
- Высокий барьер для изменений
- Тесты ломаются при рефакторинге
# Тест модели (пониженная передача)
def test_prefers_current_stock_batches_to_shipments():
in_stock_batch = Batch("in-stock", "RETRO-CLOCK", 100, eta=None)
shipment_batch = Batch("shipment", "RETRO-CLOCK", 100, eta=tomorrow)
line = OrderLine("oref", "RETRO-CLOCK", 10)
allocate(line, [in_stock_batch, shipment_batch])
assert in_stock_batch.available_quantity == 90
assert shipment_batch.available_quantity == 100 # Не изменилась
Повышенная передача — тесты сервисного слоя
Когда использовать:
- Добавление функций (add_stock, cancel_order)
- Исправление багов
- Рефакторинг без изменения модели
Преимущества:
- Меньше связаны с реализацией
- Легче рефакторинг модели
- Проверяют поведение системы
Недостатки:
- Меньше деталей о дизайне объектов
# Тест сервиса (повышенная передача)
def test_prefers_warehouse_batches_to_shipments():
in_stock_batch = Batch("in-stock", "RETRO-CLOCK", 100, eta=None)
shipment_batch = Batch("shipment", "RETRO-CLOCK", 100, eta=tomorrow)
repo = FakeRepository([in_stock_batch, shipment_batch])
session = FakeSession()
line = OrderLine('oref', "RETRO-CLOCK", 10)
services.allocate(line, repo, session)
assert in_stock_batch.available_quantity == 90
Почему тесты сервиса лучше для рефакторинга?
Проблема: тесты модели тесно связаны с реализацией. Изменили внутренний метод — сломали 10 тестов.
Решение: тесты сервиса проверяют API, а не внутренности.
Тесты API (E2E)
↓ Широкий охват, низкая эффективность изменений
Тесты сервисного слоя
↓ Баланс охвата и гибкости
Тесты предметной области
↓ Точечный охват, высокая эффективность изменений
Правило: каждая строка кода в тесте — как капля клея, удерживающая систему в определённой форме. Чем больше низкоуровневых тестов, тем труднее менять дизайн.
Устранение связей с предметной областью
Проблема: тесты зависят от объектов модели
# Сервисный слой принимает OrderLine
def allocate(line: OrderLine, repo, session) -> str:
# Тест вынужден создавать OrderLine
line = OrderLine("oref", "SKU", 10)
services.allocate(line, repo, session)
Если изменим OrderLine — сломаются все тесты сервиса.
Решение 1: использовать примитивы
# После рефакторинга
def allocate(orderid: str, sku: str, qty: int, repo, session) -> str:
# Тест с примитивами
result = services.allocate("o1", "SKU", 10, repo, session)
assert result == "batch-1"
Теперь тесты не зависят от класса OrderLine.
Решение 2: фабричные функции
class FakeRepository(set):
@staticmethod
def for_batch(ref, sku, qty, eta=None):
return FakeRepository([model.Batch(ref, sku, qty, eta)])
# В тесте
repo = FakeRepository.for_batch("batch1", "LAMP", 100)
Все зависимости от модели собраны в одном месте.
Решение 3: служба add_batch
Добавляем службу для создания партий — тесты не работают с репозиторием напрямую:
# services.py
def add_batch(ref, sku, qty, eta, repo, session):
repo.add(model.Batch(ref, sku, qty, eta))
session.commit()
# test_services.py
def test_allocate_returns_allocation():
repo, session = FakeRepository([]), FakeSession()
# Используем службу, а не репозиторий напрямую
services.add_batch("batch1", "LAMP", 100, None, repo, session)
result = services.allocate("o1", "LAMP", 10, repo, session)
assert result == "batch1"
Преимущество: тесты зависят только от сервисного слоя. Можно рефакторить модель без изменений в тестах.
Улучшение сквозных тестов
Добавляем API endpoint для add_batch:
# flask_app.py
@app.route("/add_batch", methods=['POST'])
def add_batch():
session = get_session()
repo = repository.SqlAlchemyRepository(session)
services.add_batch(
request.json['ref'],
request.json['sku'],
request.json['qty'],
request.json['eta'],
repo, session
)
return 'OK', 201
Теперь сквозные тесты не используют прямой SQL:
# test_api.py
def post_to_add_batch(ref, sku, qty, eta):
url = config.get_api_url()
r = requests.post(f'{url}/add_batch', json={
'ref': ref, 'sku': sku, 'qty': qty, 'eta': eta
})
assert r.status_code == 201
def test_happy_path():
# Подготовка через API, не через SQL
post_to_add_batch("batch1", "SKU", 100, "2022-01-01")
data = {'orderid': 'o1', 'sku': 'SKU', 'qty': 10}
r = requests.post(f'{API_URL}/allocate', json=data)
assert r.status_code == 201
assert r.json()['batchref'] == "batch1"
Эмпирические правила для тестов
Сквозные (E2E) — 1 тест на функцию. Проверка интеграции.
Сервисный слой — основная масса тестов. Бизнес-логика, крайние случаи.
Модель предметной области — малое ядро тестов. Сложная логика, документация.
Правила:
- Один сквозной тест на одну функцию сервиса
- Основная масса тестов — сервисный слой
- Малое ядро тестов модели — можно удалять, если функциональность покрыта сервисом
Выводы
- Пирамида тестов — больше юнит-тестов, меньше E2E
- Пониженная передача — тесты модели для сложной логики
- Повышенная передача — тесты сервиса для обычных задач
- Примитивы в сервисном слое — устраняют зависимости от модели
- Службы для тестов — add_batch помогает изолировать тесты
- Можно удалять тесты — если функциональность покрыта на более высоком уровне
Вопросы
- Что такое «повышенная» и «пониженная» передачи в TDD?
- Почему тесты сервисного слоя лучше для рефакторинга?
- Как устранить зависимость тестов от объектов предметной области?
- Когда писать тесты модели, а когда — сервисного слоя?
- Зачем добавлять службу add_batch только для тестов?
- Что такое тестовая пирамида и почему она важна?
