JavaRush /Курсы /Claude code /AI-assisted test generation и TDD loop

AI-assisted test generation и TDD loop

Claude code
19 уровень , 1 лекция
Открыта

1. Провести риск через первый цикл

Теперь задача — провести выбранный риск через первый TDD-цикл и не дать Claude полезть в production-код раньше времени. Широкий запрос «напиши тесты» уводит его к дешёвым happy-path проверкам — это вы уже видели.

Когда нужно увидеть сам этот ритм без шума от HTTP, БД и побочных эффектов, полезно временно взять компактный сценарий: дальше смотрю на небольшой validator, а денежный refund flow оставляю главным ориентиром по риску.

2. TDD — короткий инженерный ритм, а не магия

Про TDD часто говорят так, будто это либо великая истина, либо религиозный спор на кухне между разработчиками. На практике всё намного спокойнее: он нужен, чтобы вы и Claude двигались в безопасном порядке — сначала проверяемое поведение, потом код, украшения напоследок.

flowchart TD
    A[Ожидаемое поведение] --> B[Red: падающий тест]
    B --> C[Green: минимальный рабочий код]
    C --> D[Refactor: маленькая чистка]
    D --> E[Повтор цикла]

Для AI-assisted разработки это особенно полезно, потому что Claude по умолчанию любит быть полезным слишком широко: попросили тест — он и код поправил; попросили фикс — «немного почистил» соседний модуль. TDD убирает расплывчатость: у каждого шага свой смысл, через него не перескакивают.

Ниже — удобная таблица, которую стоит держать в голове во время работы:

Шаг Что происходит Чем помогает Claude Code Что решаете вы
Red Пишется тест на ожидаемое поведение Находит существующий стиль тестов, пишет каркас теста, предлагает assertions Что именно должно считаться правильным поведением и почему тест должен падать
Green Код приложения меняется минимально Предлагает самый короткий фикс в пределах scope Достаточен ли фикс, не вышел ли diff за границы задачи
Refactor Небольшая чистка после green Помогает упростить имена, убрать дублирование, привести тест к стилю проекта Нужна ли чистка вообще и не превращается ли она в отдельную задачу

Видно главное: Claude ускоряет почти каждый шаг, но порядок шагов и критерии перехода между ними задаёте вы.

3. Красный этап: тест должен честно упасть

На красном этапе главное, чтобы тест честно различал старое и новое поведение. В маленьком validator'е это особенно хорошо видно, поэтому смотрю не на весь refund flow, а на одно правило.

Самая важная часть всей петли — не зелёный тест, а честный красный. Многие новички интуитивно гонятся за зелёной галочкой, но без правильного красного шага она ничего не доказывает: сообщает лишь, что тест не возражает. А почему — отдельная, иногда очень грустная история.

Возьмём простой сценарий из Commerce OS: в test strategy для формы заказа зафиксировано — пустой email блокирует отправку и показывает понятную ошибку. Вот так может выглядеть слабый тест по расплывчатому запросу:

import { describe, expect, it } from "vitest";
import { validateCheckoutForm } from "./checkout-validator";

describe("validateCheckoutForm", () => {
  it("проверяет форму", () => {
    const result = validateCheckoutForm({ email: "", city: "Москва" });
    expect(result).toBeDefined(); // тест зелёный почти при любом поведении
  });
});

Такой тест выглядит прилично, но пользы в нём примерно как от зонтика в серверной. Функция что-то вернула — отлично. Только баг с пустым email он не ловит.

А вот уже тест, который действительно выражает ожидаемое поведение:

import { describe, expect, it } from "vitest";
import { validateCheckoutForm } from "./checkout-validator";

describe("validateCheckoutForm", () => {
  it("не отправляет форму без email", () => {
    const result = validateCheckoutForm({ email: "", city: "Москва" });
    expect(result.errors.email).toBe("Укажите email"); // до фикса тест должен упасть
  });
});

Здесь уже появляется смысл: мы проверяем не «функция существует», а на пустом email возникает определённая ошибка. Тест должен падать до фикса — и падать по правильной причине.

Вот это место очень важное: красный должен быть честно красным, а не «технически упавшим». Падение из-за кривого импорта, сломанной фикстуры, опечатки в пути или несуществующего mock-а — технический шум. Вам нужен сигнал на уровне поведения.

Поэтому полезно давать Claude Code такой запрос:

Прочитай TASK_SPEC.md и текущий validator.
Сначала напиши падающий тест на ожидаемое поведение.
Код приложения пока не меняй.
Коротко объясни, почему тест должен упасть на текущей реализации.

Точная формулировка может отличаться, и это нормально: важен не магический набор слов, а два ограничения: сначала только тест и объясни, почему он должен падать. Пока Claude не объясняет причину падения, доверять тесту рано.

Важное правило: пока у вас нет честно падающего теста, Claude не должен менять код приложения.
Иначе вы рискуете получить зелёный результат без доказательства, что тест вообще отличал «до» от «после».

4. Claude снимает рутину, а не думает за вас

Claude особенно полезен не там, где думает за вас, а там, где снимает рутину. Чем точнее вы держите смысловые решения за собой, тем лучше он закрывает утомительную механическую часть тестовой работы.

Полезно смотреть на это так:

Claude Code ускоряет За вами остаётся решение
Читает существующие тесты и копирует стиль проекта Какое поведение должно считаться правильным
Быстро пишет imports, describe/it и каркас теста Почему именно этот сценарий важен для текущего diff
Подбирает boilerplate для фикстур и mock-ов Не подменяет ли mock то, что вы как раз хотели проверить
Предлагает осмысленные имена тестов Выражает ли тест наблюдаемое пользователем поведение, а не внутреннюю реализацию
Может сгенерировать несколько вариантов assertions Какая проверка действительно ловит баг, а не создаёт ложное спокойствие
Напоминает про граничные случаи Какие граничные случаи уместны именно под ваш риск, а не «всё подряд»

То есть Claude особенно хорош в роли очень быстрого технического ассистента. Он мгновенно находит соседние тесты, повторяет соглашения репозитория, не забывает импортировать нужные вещи и экономит вам время на механике. Это очень много. Но это не означает, что он должен сам решать, что для пользователя в этой задаче является правильным поведением.

Практически это означает ещё одну полезную привычку: тестовую работу удобно выносить в отдельный чистый фокус. Если текущая implementation-сессия уже забита гипотезами, логами и спорными направлениями, попросите Claude в свежем контексте посмотреть только на тест. Так вы уменьшаете шанс, что старая неудачная гипотеза прилипнет и к тестам.

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

Проверь только что сгенерированный тест.
Ответь явно:
1. Он проверяет поведение пользователя или внутреннюю реализацию?
2. Может ли он пройти, если баг всё ещё существует?
3. Почему именно эта проверка важнее соседних?
Файлы не меняй.

Такой read-only шаг очень полезен: он превращает «ну вроде тест нормальный» в инженерный разговор с конкретными вопросами.

5. Зелёный этап: минимальный код вместо подвига

На зелёном шаге нужен не подвиг, а минимальный рабочий фикс. И здесь AI хочется одновременно похлопать по плечу и привязать к стулу: Claude обожает «помочь с запасом» — поправить код, улучшить названия, обновить соседние helper'ы, сделать пару добрых дел, о которых вы не просили. Цель скучнее и ценнее: красный стал зелёным, diff не распух.

Для нашего примера с проверкой email минимальный код может выглядеть так:

export function validateCheckoutForm(data: { email: string; city: string }) {
  const errors: Record<string, string> = {};

  if (!data.email.trim()) {
    errors.email = "Укажите email";
  }
  return { errors };
}

Это очень маленький кусок логики, и в этом как раз его сила: код не перепридумывает систему валидации, не вводит абстракции, не трогает соседние поля — закрывает поведение, уже выраженное тестом.

Хорошо работает такой ритм общения с Claude Code:

Исправь только поведение, из-за которого падает тест на пустой email.
Не меняй структуру модуля.
Не добавляй новые зависимости.
После изменения коротко покажи diff и скажи, какой тест должен стать зелёным.

Если после этого Claude внезапно начинает переносить validators в отдельную папку, придумывать helper и улучшать архитектуру формы, это не повод восхищаться, а повод остановить итерацию и сузить рамки. В тестировании, как в ремонте квартиры, «мы тут заодно немного переделали» звучит тревожнее, чем кажется.

6. Рефакторинг после зелёного: только при сигнале

Когда тест стал зелёным, возникает естественное желание привести всё в идеальный вид. Желание хорошее, но без дисциплины оно очень быстро превращается в «раз уж мы здесь, давайте почистим ещё пять файлов» — уже не завершение цикла, а новый рабочий день под другим названием.

Поэтому правило простое: после green допускается только маленький рефакторинг, не меняющий доказанное поведение. Назвать переменную лучше, вынести повторяющуюся проверку в helper, привести тест к стилю пакета, убрать дублирование внутри теста. Но как только cleanup начинает жить своей жизнью — остановитесь: не умещается в пару причин, значит, достоин отдельной задачи. В AI-режиме это особенно важно: Claude делает код «чище на вид», одновременно усложняя историю изменения для review.

Практически полезно держать в голове такой вопрос: если убрать этот cleanup из diff, основная задача останется выполненной? Если да — выносите отдельно, не смешивайте с bugfix. Именно поэтому чтение diff после зелёного шага не менее важно, чем запуск теста: зелёная лампочка говорит, что поведение проходит, а diff — насколько понятно и безопасно вы к этому пришли.

7. Проверка AI-сгенерированного теста на ложь

Самый опасный момент в AI-assisted тестировании — не красный тест, а уверенно зелёный, который ничего не защищает. Красный хотя бы честно сообщает: «у нас проблема». По-настоящему опасен зелёный, который создаёт комфортное, почти домашнее чувство прогресса — этим и вреден.

Полезно держать перед глазами главный вопрос лекции: Could this test pass while the bug still exists?

Или по-русски: мог бы этот тест пройти, если баг всё ещё жив?

Если ответ «да» — тест слабый, даже если он красивый, быстрый и без опечаток.

Вот несколько типичных ловушек:

Слабая проверка Почему опасно Что лучше сделать
expect(result).toBeDefined()
Функция может вернуть что угодно, баг останется Проверять конкретную ошибку, значение или блокировку действия
expect(saveOrder).toHaveBeenCalled()
Это проверка внутренней реализации, а не результата для пользователя Проверять, что форма не отправилась и пользователь увидел сообщение об ошибке
Snapshot на весь объект Может ломаться из-за шума и при этом не ловить бизнес-баг Ассертить 1–2 ключевых свойства поведения
Слишком агрессивный mock Вы замокали именно то, что хотели проверить Оставить реальную зависимость или сузить mock до периферии
Тест зелёный до фикса Он не различает старое и новое поведение Сделать сценарий, который падает именно на текущей проблеме

На практике это означает, что AI-сгенерированный тест нужно ревьюить так же внимательно, как AI-сгенерированный production-код, а иногда внимательнее: плохой тест не просто бесполезен — он легитимизирует плохой код. Подписывает бумагу «всё в порядке», хотя в комнате пахнет дымом.

Очень помогает отдельный read-only запрос на качество теста:

Оцени только качество теста, а не код приложения.
Скажи:
- проверяет ли он наблюдаемое поведение;
- не привязан ли он к внутренней реализации;
- может ли он пройти при сохранении бага;
- не слишком ли много здесь mock-логики.
Файлы не меняй.

И ещё одна практичная мелочь: если тест корректен, но имя сценария не объясняет, что проверяется, — тоже тревожный сигнал. Название должен работать корректно почти всегда означает, что автор сам не понял, что доказал. У хорошего теста ясность начинается уже с имени.

8. Правила в проекте: фиксируем раз и навсегда

Если вы уже несколько раз поймали себя на одном и том же разговоре с Claude — «сначала падающий тест», «не трогай код до теста», «после green не делай широкий cleanup» — значит, правило пора вынести из головы в проектную память, иначе будете каждый раз заново проводить тот же мини-урок инструменту.

Для этого очень хорошо подходит CLAUDE.md внутри Workflow Kit или самого проекта. Базовый кусок с тестовыми правилами может быть таким:

## Правила тестирования

- При багфиксе сначала пишем падающий regression test.
- Пока тест не написан, код приложения не меняем.
- После green допускается только маленький refactor в пределах текущего diff.
- В отчёте Claude всегда перечисляет:
  - какие тесты добавлены,
  - почему они падали,
  - что именно стало зелёным.

Такой фрагмент не делает Claude волшебно безошибочным, но резко снижает шанс, что новая сессия поедет в «сейчас всё быстренько поправлю и заодно улучшу архитектуру». А ещё это хороший сигнал команде: тестирование здесь — часть инженерного контракта, а не приложение.

И в этом, пожалуй, самая практичная сторона сегодняшней темы. Когда есть test strategy, красный шаг честный, зелёный маленький, а правила TDD зафиксированы, Claude перестаёт быть автоматом с сюрпризами и становится очень быстрым помощником внутри понятного ритма. Для инженера это ценнее любой эффектной магии.

9. TDD как каноничный кейс для режима «по цели»

Всё, что мы разбирали до этого, — пошаговый TDD-цикл: вручную пишете падающий test, просите Claude реализовать минимальный код, читаете diff, запускаете тест, идёте дальше. Это режим по умолчанию, и для большинства задач он — именно то, что нужно. Сейчас мы аккуратно покажем второй режим: он мелькал в уровне 6 как ментальная модель «работа по цели», здесь применяется впервые буквально.

Идея проста. Падающий test — это и есть машинно-проверяемое условие финиша: «этот test проходит, все остальные не падают, lint зелёный». Раз так, Claude может крутить «красный → зелёный → рефакторинг» несколько ходов подряд без вашего подтверждения, пока не достигнет условия или не упрётся в блокер. Реализация (интерактивный запрос, slash-команда вроде /goal, скриптованный цикл через claude -p) зависит от версии; важна именно ментальная модель: опишите финиш так, чтобы машина сама проверила его достижение.

Почему TDD — самый чистый случай для работы «по цели»:

  • условие финиша детерминированное — test либо проходит, либо нет (./gradlew test --tests "OrderRefundTest" возвращает чёткий exit code);
  • diff ограничен заранее: изменения в производственном коде и в тестах идут в известные файлы, расползание задачи ловится автоматически;
  • регрессии ловятся существующим набором тестов на каждом ходе, если цикл запускает их как часть проверки;
  • неудача — это явный отказ, а не «Claude сказал, что всё работает». Для кода, который сгенерировал AI, это критично.

Где режим «по цели» ломается и где нужен пошаговый:

Риск мутации теста. Если в разрешённые файлы попадают и производственный код, и сам test-файл, Claude может «починить» test, изменив проверки, вместо того чтобы пройти его реализацией. Защита: запретить запись в test-файл после того, как написан падающий test (некоторые сценарии поддерживают --no-edit-tests или эквивалент; доступность зависит от версии). Альтернатива — раздельные ходы для написания теста и для починки кода, что фактически возвращает вас к пошаговому режиму.

Субъективные критерии приёмки. «UX лучше», «текст звучит естественнее», «архитектура чище» — это не машинно-проверяемое; режим «по цели» превращается в неуправляемое переписывание. Для таких критериев нужен ручной цикл с человеком.

Дизайн теста против генерации теста. Режим «по цели» подходит для реализации поведения к уже существующему правильному test. Если test сам неверный — проверяет деталь реализации, а не поведение (см. раздел 7 про ложь AI-теста), — режим «по цели» закрепит ошибку быстрее, чем ручной цикл. Дизайн тестов машине не делегируется.

Чувствительная зона (уровень 23, тема 1 — высокий риск). Аутентификация, платежи, миграции — даже с tests требуют явного одобрения человека на каждый diff; режим «по цели» здесь неуместен, цена ошибки слишком велика.

Хороший запрос для работы «по цели» в TDD на Commerce OS:

Goal: test в tests/orders/refund_test.ts проходит,
      все остальные tests продолжают проходить,
      lint и type-check зелёные.

Constraints:
- меняй только src/orders/refund.ts и связанные файлы внутри src/orders/;
- не модифицируй ни один файл с *test* в имени;
- остановись и спроси, если цель недостижима после 2 попыток;
- остановись и спроси, если нужно менять файлы вне src/orders/;
- после каждой попытки запускай целевой test и полный lint.

Работа «по цели» в TDD — это первая и самая безопасная точка для знакомства с этим режимом в курсе. Все остальные применения (управляемая реализация в уровне 18, пилотная миграция в уровне 29, рефакторинг в уровне 27) — тот же режим в более широких границах с дополнительными ограничениями. Освоив его на TDD, вы переносите базовую модель на другие задачи в рамках границ из уровня 6.

1
Задача
Claude code, 19 уровень, 1 лекция
Недоступна
Минимальный green-step после готового failing test
Минимальный green-step после готового failing test
1
Задача
Claude code, 19 уровень, 1 лекция
Недоступна
Добавление failing test до исправления бага
Добавление failing test до исправления бага
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ