Enforces строгую разработку через тесты: тесты обязательно до кода реализации. Не позволяет писать production код без покрывающих тестов.
npx -y skills add obra/superpowers --skill test-driven-development --agent claude-codeСначала напишите тест. Посмотрите, как он падает. Напишите минимальный код, чтобы он прошёл.
Главный принцип: если вы не видели, как тест падает, вы не знаете, тестирует ли он нужное.
Нарушение буквы правил — это нарушение их духа.
Всегда:
Исключения (спросите партнёра-человека):
Думаете «пропущу TDD только в этот раз»? Стоп. Это самооправдание.
НИКАКОГО ПРОДАКШЕН-КОДА БЕЗ СНАЧАЛА ПАДАЮЩЕГО ТЕСТА
Написали код до теста? Удалите его. Начните заново.
Без исключений:
Реализуйте заново из тестов. Точка.
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;
}
Напишите один минимальный тест, показывающий, что должно произойти.
Хорошо (ясное имя, тестирует реальное поведение, одно дело):
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);
});
Требования: одно поведение; ясное имя; реальный код (без моков, если их можно избежать).
ОБЯЗАТЕЛЬНО. Никогда не пропускайте.
npm test path/to/test.test.ts
Убедитесь: тест падает (а не ошибается); сообщение о провале ожидаемое; падает из-за отсутствия функции (а не из-за опечаток).
Тест проходит? Вы тестируете существующее поведение. Поправьте тест. Тест ошибается? Исправьте ошибку, перезапускайте, пока не упадёт правильно.
Напишите простейший код, чтобы тест прошёл.
Хорошо (ровно столько, чтобы пройти):
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 */ }
Не добавляйте функции, не рефакторьте чужой код и не «улучшайте» сверх теста.
ОБЯЗАТЕЛЬНО. Убедитесь: тест проходит; другие тесты по-прежнему проходят; вывод чистый (без ошибок и предупреждений). Тест падает? Чините код, а не тест. Другие тесты падают? Чините сейчас.
Только после зелёного: уберите дублирование; улучшите имена; вынесите хелперы. Держите тесты зелёными. Не добавляйте поведение.
Следующий падающий тест для следующей функции.
| Качество | Хорошо | Плохо |
|---|---|---|
| Минимальный | Одно дело. «and» в имени? Разделите. | test('validates email and domain and whitespace') |
| Ясный | Имя описывает поведение | test('test1') |
| Показывает намерение | Демонстрирует желаемый API | Скрывает, что код должен делать |
«Напишу тесты потом, чтобы убедиться, что работает» — тесты, написанные после кода, проходят сразу. Прохождение сразу ничего не доказывает: могут тестировать не то; могут тестировать реализацию, а не поведение; могут упустить забытые краевые случаи; вы ни разу не видели, как тест ловит баг. Тест-сначала заставляет увидеть провал теста, доказывая, что он реально что-то тестирует.
«Я уже вручную протестировал все краевые случаи» — ручное тестирование стихийно. Нет записи о проверенном; нельзя перезапустить при изменении кода; легко забыть случаи под давлением. Автотесты систематичны.
«Удалять X часов работы расточительно» — ошибка невозвратных затрат. Время уже потрачено. Выбор сейчас: удалить и переписать с TDD (ещё X часов, высокая уверенность) или оставить и добавить тесты потом (30 минут, низкая уверенность, вероятные баги). «Расточительство» — это держать код, которому нельзя доверять.
«TDD догматичен, прагматизм — это адаптация» — TDD И ЕСТЬ прагматизм: находит баги до коммита; предотвращает регрессии; документирует поведение; включает рефакторинг. «Прагматичные» срезки = отладка в продакшене = медленнее.
«Тесты-после достигают тех же целей» — нет. Тесты-после отвечают «что это делает?», тесты-сначала — «что это должно делать?». Тесты-после смещены вашей реализацией. 30 минут тестов после ≠ TDD.
| Отговорка | Реальность |
|---|---|
| «Слишком просто, чтобы тестировать» | Простой код ломается. Тест — 30 секунд. |
| «Протестирую после» | Тесты, проходящие сразу, ничего не доказывают. |
| «Тесты-после достигают тех же целей» | После = «что это делает?». Сначала = «что должно делать?». |
| «Уже протестировал вручную» | Стихийно ≠ систематично. Нет записи, нельзя перезапустить. |
| «Удалять X часов расточительно» | Невозвратные затраты. Непроверенный код — техдолг. |
| «Оставлю для справки, тесты — сначала» | Вы его адаптируете. Это тесты-после. Удалить значит удалить. |
| «Нужно сначала исследовать» | Ок. Выбросьте исследование, начните с TDD. |
| «Тяжело тестировать = дизайн неясен» | Слушайте тест. Тяжело тестировать = тяжело использовать. |
| «TDD меня замедлит» | TDD быстрее отладки. Прагматично = тест-сначала. |
Всё это означает: удалить код. Начать заново с 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
Никаких исключений без разрешения вашего партнёра-человека.