Где TDD окупается, а где нет — Примеры

Пример 1 — Сообщение об ошибке, исправленной по TDD

Поддержка сообщает: купон на скидку срабатывает на подарочных картах, а они всегда должны уходить по номиналу, и починка здесь — защита в две строки. Покажите, в каком порядке должна идти работа и что она оставляет после себя.

// сообщение и код, где живёт ошибка
// "на подарочные карты попадает скидка SAVE10"
function priceOf(item, coupon) {
  return item.price * coupon.rate(item);
}

Решение

// 1. красный: воспроизвести, прежде чем трогать код
test("ignores coupons on gift cards", () => {
  expect(priceOf(giftCard(50), SAVE10)).toBe(50);
});
// FAIL: expected 50, received 45     <- дефект подтверждён

// 2. зелёный: наименьшая защита
function priceOf(item, coupon) {
  if (item.kind === "gift-card") return item.price;
  return item.price * coupon.rate(item);
}

Провал теста — единственное доказательство, что ошибка была, и единственная защита от её возвращения; вот почему исправление ошибок — самый сильный случай для цикла. Тест, написанный после починки, регрессию бы поймал, однако диагноз он не подтвердил бы никогда, а защита без воспроизведённого провала остаётся догадкой о том, какое условие было важным. Цикл здесь не церемония: именно он превращает отчёт в названный и постоянный случай.

Пример 2 — Связка, которую модульные тесты только делают хрупкой

Роутер регистрирует обработчики при старте, и проверяющий просит покрыть модульными тестами порядок регистрации. Покажите, что утверждал бы такой тест. И объясните, почему набор лучше покрыт в другом месте.

export function registerRoutes(router, handlers) {
  for (const h of handlers) router.add(h.method, h.path, h.fn);
}
// предполагаемый тест: expect(router.add).toHaveBeenNthCalledWith(1, "GET", "/health", fn)

Решение

// интеграционный тест: запустить систему и пересечь настоящий стык
test("GET /health answers 200", async () => {
  const app = await startApp({ handlers: handlersForTest });
  const res = await app.request("/health");
  expect(res.status).toBe(200);
});

У функции регистрации почти нет собственного поведения: она переправляет аргументы дальше, и модульный тест может утверждать только порядок вызовов — деталь реализации, которая ломается при каждой перестройке связки и ничего не говорит о том, доходит ли запрос до обработчика. Запустите систему и сделайте запрос: только на этом уровне правильность связки и видна. Тест раньше кода — инструмент для правил и расчётов, а не налог на каждую функцию, которая существует.