Цикл «красный — зелёный — рефакторинг» — Примеры
Пример 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);
});
Проблема была не в тесте: модуль сам тянется к системным часам, и из-за этого результат нельзя закрепить. В этом и состоит давление на проектирование — самая крупная практическая выгода от того, что тесты пишут раньше кода. Если передать часы извне, тест починится, а класс станет лучше: зависимость теперь видна в конструкторе, а не спрятана внутри метода. Такой рефакторинг делайте при зелёном наборе и никогда — при красном.