test-driven-developmentстрогий TDD
Enforces строгую разработку через тесты: тесты обязательно до кода реализации. Не позволяет писать production код без покрывающих тестов.
Установка
npx -y skills add obra/superpowers --skill test-driven-development --agent claude-codeРазработка через тесты (TDD)
Обзор
Сначала напишите тест. Посмотрите, как он падает. Напишите минимальный код, чтобы он прошёл.
Главный принцип: если вы не видели, как тест падает, вы не знаете, тестирует ли он нужное.
Нарушение буквы правил — это нарушение их духа.
Когда использовать
Всегда:
- Новые функции
- Исправления багов
- Рефакторинг
- Изменения поведения
Исключения (спросите партнёра-человека):
- Одноразовые прототипы
- Сгенерированный код
- Файлы конфигурации
Думаете «пропущу TDD только в этот раз»? Стоп. Это самооправдание.
Железный закон
НИКАКОГО ПРОДАКШЕН-КОДА БЕЗ СНАЧАЛА ПАДАЮЩЕГО ТЕСТА
Написали код до теста? Удалите его. Начните заново.
Без исключений:
- Не оставляйте «для справки»
- Не «адаптируйте» его при написании тестов
- Не смотрите на него
- Удалить значит удалить
Реализуйте заново из тестов. Точка.
Red-Green-Refactor
digraph tdd_cycle {
rankdir=LR;
red [label="RED\nWrite failing test", shape=box, style=filled, fillcolor="#ffcccc"];
verify_red [label="Verify fails\ncorrectly", shape=diamond];
green [label="GREEN\nMinimal code", shape=box, style=filled, fillcolor="#ccffcc"];
verify_green [label="Verify passes\nAll green", shape=diamond];
refactor [label="REFACTOR\nClean up", shape=box, style=filled, fillcolor="#ccccff"];
next [label="Next", shape=ellipse];
red -> verify_red;
verify_red -> green [label="yes"];
verify_red -> red [label="wrong\nfailure"];
green -> verify_green;
verify_green -> refactor [label="yes"];
verify_green -> green [label="no"];
refactor -> verify_green [label="stay\ngreen"];
verify_green -> next;
next -> red;
}
RED — напишите падающий тест
Напишите один минимальный тест, показывающий, что должно произойти.
Хорошо (ясное имя, тестирует реальное поведение, одно дело):
test('retries failed operations 3 times', async () => {
let attempts = 0;
const operation = () => {
attempts++;
if (attempts < 3) throw new Error('fail');
return 'success';
};
const result = await retryOperation(operation);
expect(result).toBe('success');
expect(attempts).toBe(3);
});
Плохо (расплывчатое имя, тестирует мок, а не код):
test('retry works', async () => {
const mock = jest.fn()
.mockRejectedValueOnce(new Error())
.mockRejectedValueOnce(new Error())
.mockResolvedValueOnce('success');
await retryOperation(mock);
expect(mock).toHaveBeenCalledTimes(3);
});
Требования: одно поведение; ясное имя; реальный код (без моков, если их можно избежать).
Проверьте RED — посмотрите, как падает
ОБЯЗАТЕЛЬНО. Никогда не пропускайте.
npm test path/to/test.test.ts
Убедитесь: тест падает (а не ошибается); сообщение о провале ожидаемое; падает из-за отсутствия функции (а не из-за опечаток).
Тест проходит? Вы тестируете существующее поведение. Поправьте тест. Тест ошибается? Исправьте ошибку, перезапускайте, пока не упадёт правильно.
GREEN — минимальный код
Напишите простейший код, чтобы тест прошёл.
Хорошо (ровно столько, чтобы пройти):
async function retryOperation<T>(fn: () => Promise<T>): Promise<T> {
for (let i = 0; i < 3; i++) {
try { return await fn(); }
catch (e) { if (i === 2) throw e; }
}
throw new Error('unreachable');
}
Плохо (переусложнено, YAGNI):
async function retryOperation<T>(
fn: () => Promise<T>,
options?: { maxRetries?: number; backoff?: 'linear' | 'exponential'; onRetry?: (attempt: number) => void; }
): Promise<T> { /* YAGNI */ }
Не добавляйте функции, не рефакторьте чужой код и не «улучшайте» сверх теста.
Проверьте GREEN — посмотрите, как проходит
ОБЯЗАТЕЛЬНО. Убедитесь: тест проходит; другие тесты по-прежнему проходят; вывод чистый (без ошибок и предупреждений). Тест падает? Чините код, а не тест. Другие тесты падают? Чините сейчас.
REFACTOR — наведите чистоту
Только после зелёного: уберите дублирование; улучшите имена; вынесите хелперы. Держите тесты зелёными. Не добавляйте поведение.
Повторите
Следующий падающий тест для следующей функции.
Хорошие тесты
| Качество | Хорошо | Плохо |
|---|---|---|
| Минимальный | Одно дело. «and» в имени? Разделите. | test('validates email and domain and whitespace') |
| Ясный | Имя описывает поведение | test('test1') |
| Показывает намерение | Демонстрирует желаемый API | Скрывает, что код должен делать |
Почему порядок важен
«Напишу тесты потом, чтобы убедиться, что работает» — тесты, написанные после кода, проходят сразу. Прохождение сразу ничего не доказывает: могут тестировать не то; могут тестировать реализацию, а не поведение; могут упустить забытые краевые случаи; вы ни разу не видели, как тест ловит баг. Тест-сначала заставляет увидеть провал теста, доказывая, что он реально что-то тестирует.
«Я уже вручную протестировал все краевые случаи» — ручное тестирование стихийно. Нет записи о проверенном; нельзя перезапустить при изменении кода; легко забыть случаи под давлением. Автотесты систематичны.
«Удалять X часов работы расточительно» — ошибка невозвратных затрат. Время уже потрачено. Выбор сейчас: удалить и переписать с TDD (ещё X часов, высокая уверенность) или оставить и добавить тесты потом (30 минут, низкая уверенность, вероятные баги). «Расточительство» — это держать код, которому нельзя доверять.
«TDD догматичен, прагматизм — это адаптация» — TDD И ЕСТЬ прагматизм: находит баги до коммита; предотвращает регрессии; документирует поведение; включает рефакторинг. «Прагматичные» срезки = отладка в продакшене = медленнее.
«Тесты-после достигают тех же целей» — нет. Тесты-после отвечают «что это делает?», тесты-сначала — «что это должно делать?». Тесты-после смещены вашей реализацией. 30 минут тестов после ≠ TDD.
Частые самооправдания
| Отговорка | Реальность |
|---|---|
| «Слишком просто, чтобы тестировать» | Простой код ломается. Тест — 30 секунд. |
| «Протестирую после» | Тесты, проходящие сразу, ничего не доказывают. |
| «Тесты-после достигают тех же целей» | После = «что это делает?». Сначала = «что должно делать?». |
| «Уже протестировал вручную» | Стихийно ≠ систематично. Нет записи, нельзя перезапустить. |
| «Удалять X часов расточительно» | Невозвратные затраты. Непроверенный код — техдолг. |
| «Оставлю для справки, тесты — сначала» | Вы его адаптируете. Это тесты-после. Удалить значит удалить. |
| «Нужно сначала исследовать» | Ок. Выбросьте исследование, начните с TDD. |
| «Тяжело тестировать = дизайн неясен» | Слушайте тест. Тяжело тестировать = тяжело использовать. |
| «TDD меня замедлит» | TDD быстрее отладки. Прагматично = тест-сначала. |
Красные флаги — СТОП и начните заново
- Код до теста
- Тест после реализации
- Тест проходит сразу
- Не можете объяснить, почему тест упал
- Тесты добавлены «позже»
- Самооправдание «только в этот раз»
- «Я уже протестировал вручную»
- «Оставлю для справки» / «адаптирую существующий код»
- «Уже потратил X часов, удалять расточительно»
- «Это другое, потому что…»
Всё это означает: удалить код. Начать заново с TDD.
Пример: исправление бага
Баг: пустой email принимается.
RED
test('rejects empty email', async () => {
const result = await submitForm({ email: '' });
expect(result.error).toBe('Email required');
});
Проверка RED
$ npm test
FAIL: expected 'Email required', got undefined
GREEN
function submitForm(data: FormData) {
if (!data.email?.trim()) { return { error: 'Email required' }; }
// ...
}
Проверка GREEN
$ npm test
PASS
REFACTOR — при необходимости вынесите валидацию для нескольких полей.
Чек-лист проверки
Прежде чем отмечать работу завершённой:
- [ ] У каждой новой функции/метода есть тест
- [ ] Видели падение каждого теста до реализации
- [ ] Каждый тест упал по ожидаемой причине (нет функции, а не опечатка)
- [ ] Написали минимальный код для прохождения каждого теста
- [ ] Все тесты проходят
- [ ] Вывод чистый (без ошибок и предупреждений)
- [ ] Тесты используют реальный код (моки только если неизбежно)
- [ ] Краевые случаи и ошибки покрыты
Не можете отметить все пункты? Вы пропустили TDD. Начните заново.
Когда застряли
| Проблема | Решение |
|---|---|
| Не знаю, как тестировать | Напишите желаемый API. Сначала утверждение. Спросите партнёра-человека. |
| Тест слишком сложный | Дизайн слишком сложный. Упростите интерфейс. |
| Нужно мокать всё | Код слишком связан. Используйте dependency injection. |
| Огромная настройка теста | Вынесите хелперы. Всё ещё сложно? Упростите дизайн. |
Интеграция с отладкой
Нашли баг? Напишите падающий тест, воспроизводящий его. Следуйте циклу TDD. Тест доказывает исправление и предотвращает регрессию. Никогда не чините баги без теста.
Антипаттерны тестирования
Добавляя моки или тестовые утилиты, прочтите testing-anti-patterns.md, чтобы избежать частых ловушек: тестирование поведения мока вместо реального; добавление методов «только для тестов» в продакшен-классы; мокирование без понимания зависимостей.
Финальное правило
Production code → test exists and failed first
Otherwise → not TDD
Никаких исключений без разрешения вашего партнёра-человека.
