Разработка через тестирование
TDD: соблюдай цикл RED-GREEN-REFACTOR, тесты до кода.
Метаданные навыка
| Источник | Встроенный (установлен по умолчанию) |
| Путь | skills/software-development/test-driven-development |
| Версия | 1.1.0 |
| Автор | VibeOS (адаптировано из obra/superpowers) |
| Лицензия | MIT |
| Платформы | linux, macos, windows |
| Теги | testing, tdd, development, quality, red-green-refactor |
| Связанные навыки | systematic-debugging, plan, subagent-driven-development |
Справочник: полный SKILL.md
Ниже приведено полное описание навыка, которое VibeOS загружает при его активации. Это те инструкции, которые видит агент, когда навык активен.
Разработка через тестирование (TDD)
Обзор
Сначала напиши тест. Убедись, что он падает. Напиши минимальный код, чтобы он прошёл.
Основной принцип: Если ты не видел, как тест падает, ты не знаешь, проверяет ли он то, что нужно.
Нарушение буквы правил — это нарушение духа правил.
Когда использовать
Всегда:
- Новые функции
- Исправление ошибок
- Рефакторинг
- Изменение поведения
Исключения (спроси пользователя):
- Одноразовые прототипы
- Сгенерированный код
- Конфигурационные файлы
Думаешь «пропущу TDD только один раз»? Остановись. Это самооправдание.
Железный закон
НИКАКОГО ПРОДАКШН-КОДА БЕЗ ПРЕДВАРИТЕЛЬНО ПАДАЮЩЕГО ТЕСТА
Написал код до теста? Удали. Начни заново.
Никаких исключений:
- Не оставляй «для справки»
- Не «адаптируй» его при написании тестов
- Не смотри на него
- Удалить — значит удалить
Реализуй заново с нуля по тестам. Точка.
Цикл Red-Green-Refactor
RED — Напиши падающий тест
Напиши один минимальный тест, показывающий ожидаемое поведение.
Хороший тест:
def test_retries_failed_operations_3_times():
attempts = 0
def operation():
nonlocal attempts
attempts += 1
if attempts < 3:
raise Exception('fail')
return 'success'
result = retry_operation(operation)
assert result == 'success'
assert attempts == 3
Понятное имя, проверяет реальное поведение, одну вещь.
Плохой тест:
def test_retry_works():
mock = MagicMock()
mock.side_effect = [Exception(), Exception(), 'success']
result = retry_operation(mock)
assert result == 'success' # А количество попыток? Тайминги?
Расплывчатое имя, тестирует мок, а не реальный код.
Требования:
- Одно поведение на тест
- Понятное описательное имя (есть «и» в имени? Раздели)
- Реальный код, а не моки (если только это действительно неизбежно)
- Имя описывает поведение, а не реализацию
Проверь RED — Убедись, что он падает
ОБЯЗАТЕЛЬНО. Никогда не пропускай.
# Используй инструмент терминала для запуска конкретного теста
pytest tests/test_feature.py::test_specific_behavior -v
Убедись:
- Тест падает (а не выдаёт ошибки из-за опечаток)
- Сообщение об ошибке ожидаемое
- Падает, потому что функция отсутствует
Тест сразу прошёл? Ты тестируешь существующее поведение. Исправь тест.
Тест выдаёт ошибку? Исправь ошибку, запускай снова, пока не будет падать корректно.
GREEN — Минимальный код
Напиши самый простой код, чтобы тест прошёл. Ничего лишнего.
Хорошо:
def add(a, b):
return a + b # Ничего лишнего
Плохо:
def add(a, b):
result = a + b
logging.info(f"Adding {a} + {b} = {result}") # Лишнее!
return result
Не добавляй функции, не рефактори другой код и не «улучшай» сверх теста.
На этапе GREEN можно жульничать:
- Жёстко прописывать возвращаемые значения
- Копировать-вставлять
- Дублировать код
- Пропускать граничные случаи
Исправим на этапе REFACTOR.
Проверь GREEN — Убедись, что он проходит
ОБЯЗАТЕЛЬНО.
# Запусти конкретный тест
pytest tests/test_feature.py::test_specific_behavior -v
# Затем запусти ВСЕ тесты, чтобы проверить регрессии
pytest tests/ -q
Убедись:
- Тест проходит
- Остальные тесты всё ещё проходят
- Вывод чистый (нет ошибок, предупреждений)
Тест не проходит? Исправляй код, а не тест.
Другие тесты не проходят? Исправляй регрессии сейчас.
REFACTOR — Приведи в порядок
Только после green:
- Удали дублирование
- Улучши имена
- Выдели вспомогательные функции
- Упрости выражения
Следи, чтобы тесты оставались зелёными на всём протяжении. Не добавляй поведение.
Если тесты падают во время рефакторинга: Немедленно отмени изменения. Делай шаги меньше.
Повторяй
Следующий падающий тест для следующего поведения. Один цикл за раз.
Избегай горизонтальных срезов
Не пиши все тесты сразу, а потом всю реализацию. Это горизонтальный срез: RED превращается в «напиши кучу придуманных тестов», а GREEN — в «заставь кучу тестов проходить». Это порождает хрупкие тесты, потому что тесты разрабатываются до того, как реализация показала, какое поведение и интерфейс на самом деле важны.
Вместо этого используй вертикальные трассирующие пули:
НЕПРАВИЛЬНО:
RED: test1, test2, test3, test4
GREEN: impl1, impl2, impl3, impl4
ПРАВИЛЬНО:
RED→GREEN: test1→impl1
RED→GREEN: test2→impl2
RED→GREEN: test3→impl3
Трассирующая пуля — это один сквозной срез поведения. Она доказывает, что путь работает, учит тебя интерфейсу и позволяет каждому следующему тесту опираться на то, что ты только что узнал.
Почему порядок важен
«Я напишу тесты после, чтобы проверить, что работает»
Тесты, написанные после кода, проходят сразу. Прохождение сразу ничего не доказывает:
- Могут проверять не то, что нужно
- Могут проверять реализацию, а не поведение
- Могут упускать граничные случаи, которые ты забыл
- Ты никогда не видел, как они ловят ошибку
Тест-first заставляет тебя увидеть, как тест падает, доказывая, что он действительно что-то проверяет.
«Я уже вручную протестировал все граничные случаи»
Ручное тестирование — это ad-hoc. Тебе кажется, что ты всё проверил, но:
- Нет записи того, что ты тестировал
- Нельзя перезапустить при изменении кода
- Легко забыть о случаях под давлением
- «Это работало, когда я попробовал» ≠ исчерпывающе
Автоматизированные тесты систематичны. Они выполняются одинаково каждый раз.
«Удалить X часов работы — это расточительно»
Ошибка невозвратных затрат. Время уже ушло. Твой выбор сейчас:
- Удалить и переписать с TDD (высокая уверенность)
- Оставить и добавить тесты после (низкая уверенность, вероятные баги)
«Расточительно» — это оставлять код, которому нельзя доверять.
«TDD догматичен, быть прагматичным — значит адаптироваться»
TDD И ЕСТЬ прагматизм:
- Находит баги до коммита (быстрее, чем отладка после)
- Предотвращает регрессии (тесты сразу ловят поломки)
- Документирует поведение (тесты показывают, как использовать код)
- Позволяет рефакторить (меняй свободно, тесты ловят поломки)
«Прагматичные» сокращения = отладка в продакшене = медленнее.
«Тесты после достигают тех же целей — это дух, а не ритуал»
Нет. Тесты-после отвечают на вопрос «Что это делает?» Тесты-до отвечают на вопрос «Что это должно делать?»
Тесты-после предвзяты из-за твоей реализации. Ты тестируешь то, что построил, а не то, что требуется. Тесты-до заставляют обнаруживать граничные случаи до реализации.
Распространённые самооправдания
| Отговорка | Реальность |
|---|---|
| «Слишком просто для теста» | Простой код ломается. Тест занимает 30 секунд. |
| «Напишу тесты после» | Тесты, проходящие сразу, ничего не доказывают. |
| «Тесты после достигают тех же целей» | Тесты-после = «что это делает?» Тесты-до = «что это должно делать?» |
| «Уже вручную протестировал» | Ad-hoc ≠ систематично. Нет записи, нельзя перезапустить. |
| «Удалить X часов — расточительно» | Ошибка невозвратных затрат. Хранение непроверенного кода — технический долг. |
| «Оставлю как справочный материал, напишу тесты сначала» | Ты будешь его адаптировать. Это тестирование после. Удалить — значит удалить. |
| «Нужно сначала исследовать» | Хорошо. Выбрось исследование, начни с TDD. |
| «Сложно тестировать = непонятный дизайн» | Слушай тест. Сложно тестировать = сложно использовать. |
| «TDD замедлит меня» | TDD быстрее, чем отладка. Прагматично = тест-first. |
| «Ручной тест быстрее» | Ручной не доказывает граничные случаи. Ты будешь перетестировать каждое изменение. |
| «У существующего кода нет тестов» | Ты его улучшаешь. Добавляй тесты для кода, который трогаешь. |
Красные флаги — ОСТАНОВИСЬ и начни заново
Если ты поймал себя на чём-то из этого, удали код и начни заново с TDD:
- Код до теста
- Тест после реализации
- Тест проходит с первого запуска
- Не можешь объяснить, почему тест упал
- Тесты добавлены «потом»
- Самооправдание «только один раз»
- «Я уже вручную протестировал»
- «Тесты после достигают той же цели»
- «Оставлю как справочный материал» или «адаптирую существующий код»
- «Уже потратил X часов, удалять расточительно»
- «TDD догматичен, я прагматичен»
- «Это другое, потому что...»
Всё это означает: Удали код. Начни заново с TDD.
Контрольный список
Перед тем как отметить работу как выполненную:
- Каждая новая функция/метод имеют тест
- Видел, как каждый тест падает до реализации
- Каждый тест падал по ожидаемой причине (функция отсутствует, а не опечатка)
- Написал минимальный код для прохождения каждого теста
- Все тесты проходят
- Вывод чистый (нет ошибок, предупреждений)
- Тесты используют реальный код (моки только если неизбежно)
- Граничные случаи и ошибки покрыты
Не можешь отметить все пункты? Ты пропустил TDD. Начни заново.
Если застрял
| Проблема | Решение |
|---|---|
| Не знаешь, как тестировать | Напиши желаемый API. Напиши сначала утверждение. Спроси пользователя. |
| Тест слишком сложный | Дизайн слишком сложный. Упрости интерфейс. |
| Нужно замокать всё | Код слишком связан. Используй внедрение зависимостей. |
| Подготовка теста огромна | Выдели вспомогательные функции. Всё ещё сложно? Упрости дизайн. |
Интеграция с VibeOS
Запуск тестов
Используй инструмент terminal для запуска тестов на каждом шаге:
# RED — проверь падение
terminal("pytest tests/test_feature.py::test_name -v")
# GREEN — проверь прохождение
terminal("pytest tests/test_feature.py::test_name -v")
# Полный набор — проверь отсутствие регрессий
terminal("pytest tests/ -q")
С delegate_task
При отправке подзадач субагентам для реализации, применяй TDD в цели:
delegate_task(
goal="Реализовать [функция] со строгим TDD",
context="""
Следуй навыку разработки через тестирование:
1. Сначала напиши ПАДАЮЩИЙ тест
2. Запусти тест, чтобы убедиться, что он падает
3. Напиши минимальный код для прохождения
4. Запусти тест, чтобы убедиться, что он проходит
5. Выполни рефакторинг при необходимости
6. Зафиксируй изменения
Команда для запуска тестов проекта: pytest tests/ -q
Структура проекта: [опиши соответствующие файлы]
""",
toolsets=['terminal', 'file']
)
С systematic-debugging
Нашёл баг? Напиши падающий тест, воспроизводящий его. Следуй циклу TDD. Тест доказывает исправление и предотвращает регрессию.
Никогда не исправляй баги без теста.
Антипаттерны тестирования
- Тестирование поведения мока вместо реального поведения — моки должны проверять взаимодействия, а не заменять тестируемую систему
- Тестирование деталей реализации — тестируй поведение/результаты, а не внутренние вызовы методов
- Только счастливый путь — всегда тестируй граничные случаи, ошибки и границы
- Хрупкие тесты — тесты должны проверять поведение, а не структуру; рефакторинг не должен их ломать
Финальное правило
Продакшн-код → тест существует и сначала упал
Иначе → это не TDD
Никаких исключений без явного разрешения пользователя.