Цикл «красный — зелёный — рефакторинг» — Примеры

Пример 1 — Красный, провалившийся по неверной причине

Тест ниже написали первым, а прогнали уже потом; вывод приведён так, как его напечатал раннер. Решите, годится ли такой красный, и дойдите до зелёного.

FAIL  tests/discount.test.ts
  ● Test suite failed to run
    Cannot find module './discout' from 'tests/discount.test.ts'

test("applies five percent after five years", () => {
  expect(discountFor(customerWith({ years: 5 }))).toBe(0.05);
});

Решение

// исправленный импорт; теперь провал тот, который нужен
import { discountFor } from "./discount";
// FAIL  discountFor is not defined            <- ожидаемый красный

// зелёный: простейшее, что проходит
export function discountFor(customer) {
  return 0.05;   // один случай; следующий тест требует правила
}

Красный из-за того, что модуля нет, ничего не говорит о поведении — он доказывает лишь, что файл импортировали. Годный красный называет то, что вы вот-вот построите, поэтому видно, что утверждение связано с кодом и действительно способно провалиться. Исправление импорта стоит минуты и покупает единственную гарантию, на которой держится весь цикл: этот тест уже один раз падал по верной причине, и он снова упадёт, если поведение даст регрессию.

Пример 2 — Модуль, который трудно тестировать

Тест сервиса регистрации, который решает, истёк ли пробный период, падает по-разному в зависимости от часа запуска. О чём говорит эта трудность? Измените код, а не утверждение.

export class Trial {
  isExpired(signupAt: Date) {
    const days = (Date.now() - signupAt.getTime()) / 86400000;
    return days > 14;
  }
}

// FAIL на некоторых прогонах: ожидалось false, получено true

Решение

export class Trial {
  constructor(private readonly now: () => Date = () => new Date()) {}

  isExpired(signupAt: Date) {
    const days = (this.now().getTime() - signupAt.getTime()) / 86400000;
    return days > 14;
  }
}

test("expires after fourteen days", () => {
  const trial = new Trial(() => new Date("2026-03-20T00:00:00Z"));
  expect(trial.isExpired(new Date("2026-03-05T00:00:00Z"))).toBe(true);
});

Проблема была не в тесте: модуль сам тянется к системным часам, и из-за этого результат нельзя закрепить. В этом и состоит давление на проектирование — самая крупная практическая выгода от того, что тесты пишут раньше кода. Если передать часы извне, тест починится, а класс станет лучше: зависимость теперь видна в конструкторе, а не спрятана внутри метода. Такой рефакторинг делайте при зелёном наборе и никогда — при красном.