Перейти к содержанию

На этой странице TDD: соблюдай КРАСНЫЙ-ЗЕЛЁНЫЙ-РЕФАКТОРИНГ, тесты до кода.

Метаданные навыка

Источник Встроенный (устанавливается по умолчанию)
Путь skills/software-development/test-driven-development
Версия 1.1.0
Автор Hermes Agent (адаптировано из obra/superpowers)
Лицензия MIT
Теги testing, tdd, development, quality, red-green-refactor
Связанные навыки systematic-debugging, writing-plans, subagent-driven-development

Справочник: полный SKILL.md

info Ниже приведено полное описание навыка, которое Hermes загружает, когда этот навык активирован. Именно эти инструкции видит агент, когда навык активен.

Разработка через тестирование (TDD)

Обзор

Сначала напиши тест. Убедись, что он падает. Напиши минимальный код, чтобы он прошёл.

Основной принцип: Если ты не видел, как тест упал, ты не знаешь, проверяет ли он то, что нужно.

Нарушение буквы правил — это нарушение духа правил.

Когда использовать

Всегда:

  • Новые функции
  • Исправления ошибок
  • Рефакторинг
  • Изменения поведения

Исключения (спроси пользователя сначала):

  • Одноразовые прототипы
  • Сгенерированный код
  • Конфигурационные файлы

Думаешь «пропущу TDD только в этот раз»? Остановись. Это самооправдание.

Железный закон

НИКАКОГО ПРОДАКШН-КОДА БЕЗ ПРЕДВАРИТЕЛЬНО ПАДАЮЩЕГО ТЕСТА

Написал код до теста? Удали. Начни заново.

Без исключений:

  • Не оставляй как «справочный материал»
  • Не «адаптируй» его при написании тестов
  • Не смотри на него
  • Удалить — значит удалить

Реализуй заново с нуля по тестам. Точка.

Цикл «Красный-Зелёный-Рефакторинг»

КРАСНЫЙ — Напиши падающий тест

Напиши один минимальный тест, показывающий, что должно происходить.

Хороший тест:

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'  # А количество повторов? Время?

Расплывчатое имя, тестирует мок, а не реальный код.

Требования:

  • Одно поведение на тест
  • Понятное описательное имя (есть «и» в имени? Раздели его)
  • Реальный код, а не моки (если только это действительно неизбежно)
  • Имя описывает поведение, а не реализацию

Проверь КРАСНЫЙ — Убедись, что падает

ОБЯЗАТЕЛЬНО. Никогда не пропускай.

# Используй инструмент terminal для запуска конкретного теста
pytest tests/test_feature.py::test_specific_behavior -v

Подтверди:

  • Тест падает (а не выдаёт ошибки из-за опечаток)
  • Сообщение об ошибке соответствует ожидаемому
  • Падает, потому что функциональность отсутствует

Тест сразу прошёл? Ты тестируешь существующее поведение. Исправь тест.

Ошибка в тесте? Исправь ошибку, запускай снова, пока тест не будет падать корректно.

ЗЕЛЁНЫЙ — Минимальный код

Напиши самый простой код, чтобы тест прошёл. Ничего лишнего.

Хорошо:

def add(a, b):
    return a + b  # Ничего лишнего

Плохо:

def add(a, b):
    result = a + b
    logging.info(f"Adding {a} + {b} = {result}")  # Лишнее!
    return result

Не добавляй функциональность, не рефакторь другой код и не «улучшай» сверх того, что требует тест.

В ЗЕЛЁНОМ можно жульничать:

  • Жёстко закодированные возвращаемые значения
  • Копипаста
  • Дублирование кода
  • Пропуск краевых случаев

Всё исправим на этапе РЕФАКТОРИНГА.

Проверь ЗЕЛЁНЫЙ — Убедись, что проходит

ОБЯЗАТЕЛЬНО.

# Запусти конкретный тест
pytest tests/test_feature.py::test_specific_behavior -v

# Затем запусти ВСЕ тесты, чтобы проверить регрессии
pytest tests/ -q

Подтверди:

  • Тест проходит
  • Остальные тесты всё ещё проходят
  • Вывод чистый (нет ошибок, предупреждений)

Тест не проходит? Исправляй код, а не тест.

Другие тесты падают? Исправляй регрессии сейчас же.

РЕФАКТОРИНГ — Приберись

Только после зелёного:

  • Удали дублирование
  • Улучши имена
  • Вынеси вспомогательные функции
  • Упрости выражения

Следи, чтобы тесты оставались зелёными на всём протяжении. Не добавляй поведение.

Если тесты падают во время рефакторинга: Отмени изменения немедленно. Делай шаги мельче.

Повторяй

Следующий падающий тест для следующего поведения. Один цикл за раз.

Почему порядок важен

«Я напишу тесты после, чтобы проверить, что работает»

Тесты, написанные после кода, проходят сразу. Прохождение сразу ничего не доказывает:

  • Могут тестировать не то
  • Могут тестировать реализацию, а не поведение
  • Могут пропустить краевые случаи, которые ты забыл
  • Ты никогда не видел, как они ловят ошибку

Тест-фёрс заставляет тебя увидеть, что тест падает, доказывая, что он вообще что-то тестирует.

«Я уже вручную протестировал все краевые случаи»

Ручное тестирование — это ad-hoc. Тебе кажется, что ты протестировал всё, но:

  • Нет записи того, что ты тестировал
  • Нельзя перезапустить при изменении кода
  • Легко забыть о случаях под давлением
  • «Работало, когда я попробовал» ≠ исчерпывающее тестирование

Автоматизированные тесты систематичны. Они выполняются одинаково каждый раз.

«Удалить X часов работы — это расточительно»

Ошибка невозвратных затрат. Время уже ушло. Твой выбор сейчас:

  • Удалить и переписать с TDD (высокая уверенность)
  • Оставить и добавить тесты после (низкая уверенность, вероятные баги)

«Расточительство» — это хранить код, которому нельзя доверять.

«TDD — это догматизм, быть прагматичным — значит адаптироваться»

TDD — ЭТО прагматично:

  • Находит баги до коммита (быстрее, чем отладка после)
  • Предотвращает регрессии (тесты сразу ловят поломки)
  • Документирует поведение (тесты показывают, как использовать код)
  • Позволяет рефакторить (меняй свободно, тесты поймают поломки)

«Прагматичные» сокращения = отладка на проде = медленнее.

«Тесты после достигают тех же целей — это дух, а не ритуал»

Нет. Тесты-после отвечают на вопрос «Что это делает?» Тесты-до отвечают на вопрос «Что это должно делать?»

Тесты-после смещены в сторону твоей реализации. Ты тестируешь то, что построил, а не то, что требуется. Тесты-до заставляют обнаруживать краевые случаи до реализации.

Распространённые самооправдания

Отговорка Реальность
«Слишком просто для теста» Простой код ломается. Тест занимает 30 секунд.
«Я протестирую после» Тесты, проходящие сразу, ничего не доказывают.
«Тесты после достигают тех же целей» Тесты-после = «что это делает?» Тесты-до = «что это должно делать?»
«Уже вручную протестировал» Ad-hoc ≠ систематично. Нет записи, нельзя перезапустить.
«Удалить X часов — расточительно» Ошибка невозвратных затрат. Хранение непроверенного кода — технический долг.
«Оставлю как справочный материал, напишу тесты сначала» Ты будешь его адаптировать. Это тестирование после. Удалить — значит удалить.
«Нужно сначала исследовать» Хорошо. Выбрось исследование, начни с TDD.
«Тест сложный = дизайн неясен» Слушай тест. Сложно тестировать = сложно использовать.
«TDD меня замедлит» TDD быстрее отладки. Прагматично = тест-фёрс.
«Ручной тест быстрее» Ручной не доказывает краевые случаи. Будешь перетестировать каждое изменение.
«В существующем коде нет тестов» Ты его улучшаешь. Добавь тесты для кода, к которому прикасаешься.

Красные флаги — ОСТАНОВИСЬ и начни заново

Если поймал себя на чём-то из этого, удали код и начни заново с TDD:

  • Код до теста
  • Тест после реализации
  • Тест проходит сразу при первом запуске
  • Не можешь объяснить, почему тест упал
  • Тесты добавлены «потом»
  • Самооправдание «только в этот раз»
  • «Я уже вручную протестировал»
  • «Тесты после достигают той же цели»
  • «Оставлю как справочный материал» или «адаптирую существующий код»
  • «Уже потратил X часов, удалять расточительно»
  • «TDD — это догматизм, я прагматичен»
  • «Это другое, потому что...»

Всё это означает: Удали код. Начни заново с TDD.

Контрольный список проверки

Перед отметкой работы как завершённой:

  • Каждая новая функция/метод имеют тест
  • Наблюдал, как каждый тест падает перед реализацией
  • Каждый тест упал по ожидаемой причине (функциональность отсутствует, а не опечатка)
  • Написал минимальный код для прохождения каждого теста
  • Все тесты проходят
  • Вывод чистый (нет ошибок, предупреждений)
  • Тесты используют реальный код (моки — только если неизбежно)
  • Краевые случаи и ошибки покрыты

Не можешь отметить все пункты? Ты пропустил TDD. Начни заново.

Когда застрял

Проблема Решение
Не знаешь, как тестировать Напиши желаемый API. Напиши сначала утверждение. Спроси пользователя.
Тест слишком сложный Дизайн слишком сложный. Упрости интерфейс.
Нужно замокать всё Код слишком связан. Используй внедрение зависимостей.
Подготовка теста огромна Вынеси вспомогательные функции. Всё ещё сложно? Упрости дизайн.

Интеграция с Hermes Agent

Запуск тестов

Используй инструмент terminal для запуска тестов на каждом шаге:

# КРАСНЫЙ — проверь, что падает
terminal("pytest tests/test_feature.py::test_name -v")

# ЗЕЛЁНЫЙ — проверь, что проходит
terminal("pytest tests/test_feature.py::test_name -v")

# Полный набор — проверь, что нет регрессий
terminal("pytest tests/ -q")

Через delegate_task

При отправке подчинённым агентам задач на реализацию, укажи TDD в цели:

delegate_task(
    goal="Implement [feature] using strict TDD",
    context="""
    Follow test-driven-development skill:
    1. Write failing test FIRST
    2. Run test to verify it fails
    3. Write minimal code to pass
    4. Run test to verify it passes
    5. Refactor if needed
    6. Commit

    Project test command: pytest tests/ -q
    Project structure: [describe relevant files]
    """,
    toolsets=['terminal', 'file']
)

С systematic-debugging

Нашёл баг? Напиши падающий тест, воспроизводящий его. Следуй циклу TDD. Тест доказывает исправление и предотвращает регрессию.

Никогда не исправляй баги без теста.

Анти-паттерны тестирования

  • Тестирование поведения мока вместо реального поведения — моки должны проверять взаимодействия, а не заменять тестируемую систему
  • Тестирование деталей реализации — тестируй поведение/результаты, а не внутренние вызовы методов
  • Только счастливый путь — всегда тестируй краевые случаи, ошибки и границы
  • Хрупкие тесты — тесты должны проверять поведение, а не структуру; рефакторинг не должен их ломать

Финальное правило

Продакшн-код → тест существует и сначала упал
Иначе → это не TDD

Без исключений без явного разрешения пользователя.