TDD с AI coding agents: как не обмануть себя тестами
Что такое TDD с AI coding agent?
TDD с AI coding agent сохраняет human-owned цикл red-green-refactor, делегируя ограниченную работу: превратить одно behavior в failing test, доказать ожидаемую причину падения, внести минимальное production change, прогнать проверки и рефакторить только на green. Agent ускоряет редактирование и запуск, но passing tests не делают его oracle правильного поведения.
TL;DR
- -TDD — последовательность маленьких behavior changes, а не batch, где AI пишет весь suite и затем всю implementation.
- -Сначала определите contract и test list. Попросите agent реализовать ровно один test и запретите production changes на red step.
- -Запустите новый test до implementation и проверьте причину failure. Compile error или не то exception не обязательно дают полезный red.
- -Не разрешайте agent ослаблять, удалять или переписывать test ради green. Любое изменение теста требует отдельного объяснения и review.
- -Coverage показывает исполнение, а не fault detection. Проверяйте важные tests на заведомо неправильной implementation или mutation testing.
- -Используйте быстрый unit cycle для deterministic behavior, затем добавляйте contract, integration и E2E evidence на границе риска.
AI coding agent способен очень быстро заставить плохой тест пройти.
Это главный риск AI-assisted TDD. Agent может написать test, production code, mocks и fixtures в одном context. Если requirement расплывчатый, обе стороны разделят одну ошибочную assumption. Green доказывает согласованность двух generated artifacts, а не корректность продукта.
Рабочий процесс сохраняет короткий feedback loop и делает каждый переход наблюдаемым.
TDD меняет по одному behavior
Знакомый цикл: red, green, refactor.
- написать следующий test для одного behavior;
- запустить и увидеть ожидаемый failure;
- написать достаточно production code для прохождения;
- прогнать релевантный suite;
- улучшить структуру без изменения поведения.
В описании TDD Martin Fowler сначала составляет test-case list, а затем выбирает один test на цикл. Последовательность tests направляет design (Fowler). Просьба сгенерировать двадцать failing tests, а затем реализовать всё сразу — test-first batch development, но не короткий TDD loop.
AI полезен тем, что быстро читает local conventions, редактирует, запускает команды и реагирует на failures. Актуальная документация Claude Code рекомендует давать конкретный context и verification target; agentic loop использует tests для проверки работы (best practices, how Claude Code works).
Agent всё равно нужен oracle: human-approved contract, example, protocol или существующее поведение.
Шаг 0. Сделайте environment детерминированным
До первого red-green cycle запишите:
Target test command:
Full verification command:
Files allowed to change:
Files that must not change:
External services:
Clock/randomness policy:
Fixture and cleanup policy:
Запустите существующий target suite. Если он уже red или flaky, зафиксируйте это отдельно. Иначе agent может принять старый failure за результат своей работы или начать «исправлять» чужие tests.
Контролируйте sources of nondeterminism:
- inject или fake time вместо sleep;
- seed randomness или изолируйте random policy;
- используйте собственные fixtures;
- не вызывайте live third-party API во внутреннем loop;
- сбрасывайте database state;
- задавайте locale и timezone явно.
Не добавляйте testing dependency только потому, что agent умеет с ним работать. Следуйте существующему framework и approval process проекта.
Шаг 1. Запишите behavior contract
Tests — executable examples, а не вся спецификация. Начните с observable rules и exclusions.
Пример для функции задержки retries:
Function:
calculateRetryDelay(attempt, baseDelayMs, maxDelayMs) -> number
Rules:
- attempt — zero-based non-negative integer
- delay удваивается для каждого следующего attempt
- result не превышает maxDelayMs
- baseDelayMs и maxDelayMs — positive integers
- maxDelayMs должен быть не меньше baseDelayMs
- invalid input вызывает RangeError
- jitter в этой функции отсутствует
Non-goals:
- решение, можно ли повторять operation
- sleep
- logging
- чтение environment variables
Хороший prompt ссылается на repository, а не пересказывает его по памяти:
Прочитай существующие tests и implementation conventions:
- src/retry/
- test/retry/
По утверждённому contract ниже предложи ordered test list.
Не редактируй files и не проектируй extra behavior.
Для каждого test укажи observable rule и почему он следующий.
[contract]
Проверьте список. Удалите дублирующие examples и придуманные requirements. Выберите минимальный test, двигающий design.
Шаг 2. Создайте ровно один red test
Prompt:
Добавь только первый test из approved list.
Constraints:
- измени только назначенный test file;
- не создавай и не редактируй production code;
- следуй существующим Vitest conventions;
- проверяй public behavior, а не private implementation;
- запусти: npm test -- retry-delay.test.ts;
- покажи точный failing assertion или error.
Остановись после доказательства red.
Первый test:
import { describe, expect, it } from 'vitest';
import { calculateRetryDelay } from './retry-delay';
describe('calculateRetryDelay', () => {
it('returns the base delay for attempt zero', () => {
expect(calculateRetryDelay(0, 100, 1_000)).toBe(100);
});
});
Проверьте три вещи:
- test падает до production change;
- причина — отсутствующее behavior;
- failure исчезнет только при изменении observable behavior.
Module not found допустим для самого первого slice, но syntax error, broken fixture или отсутствующая test dependency не доказывают product behavior. Если test сразу проходит, разберитесь. Не изображайте состоявшийся red step.
Для bugfix сначала воспроизведите user-visible defect:
Добавь один regression test, который падает на текущей branch
и представляет:
[observed input, state и expected output].
Не редактируй production code.
Пока не обобщай за пределы incident.
Запусти narrow test и покажи failure.
Шаг 3. Внесите минимальное green change
После review red:
Внеси минимальное production change, проходящее новый test.
Constraints:
- не изменяй, не удаляй, не skip и не ослабляй tests;
- не добавляй незапрошенное behavior;
- сохрани public API в approved contract;
- запусти narrow test, затем существующий retry suite;
- остановись и объясни, если сам test кажется неправильным.
Минимальная implementation намеренно мала:
export function calculateRetryDelay(
attempt: number,
baseDelayMs: number,
maxDelayMs: number,
): number {
return baseDelayMs;
}
Этого достаточно для первого behavior, но не для финального algorithm.
Проверяйте diff, а не только exit code. Agents иногда получают green изменением fixture, расширением matcher, заменой реальной dependency на mock или поглощением проверяемого error. Не смешивайте test changes с green commit без явного human approval.
Шаг 4. Продолжайте с boundaries
Добавляйте по одному behavior за цикл:
it('doubles the delay for each attempt', () => {
expect(calculateRetryDelay(1, 100, 1_000)).toBe(200);
expect(calculateRetryDelay(2, 100, 1_000)).toBe(400);
});
it('caps the delay at the configured maximum', () => {
expect(calculateRetryDelay(5, 100, 1_000)).toBe(1_000);
});
it.each([
[-1, 100, 1_000],
[0.5, 100, 1_000],
[0, 0, 1_000],
[0, 100, 99],
])('rejects invalid inputs', (attempt, baseDelayMs, maxDelayMs) => {
expect(() =>
calculateRetryDelay(attempt, baseDelayMs, maxDelayMs),
).toThrow(RangeError);
});
До принятия предложенного agent edge case спросите, какое contract rule или failure mode его обосновывает. null, empty arrays, timeouts, malformed UTF-8 и concurrency — не универсальные украшения. Они нужны, только если возможны на type boundary, в runtime или domain.
Финальная implementation:
export function calculateRetryDelay(
attempt: number,
baseDelayMs: number,
maxDelayMs: number,
): number {
const hasInvalidInput =
!Number.isInteger(attempt) ||
attempt < 0 ||
!Number.isInteger(baseDelayMs) ||
baseDelayMs <= 0 ||
!Number.isInteger(maxDelayMs) ||
maxDelayMs < baseDelayMs;
if (hasInvalidInput) {
throw new RangeError('Invalid retry delay parameters');
}
return Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
}
При очень большом attempt выражение может переполниться до Infinity; Math.min всё равно вернёт cap при finite valid configuration. Нужно ли запрещать unsafe integers — отдельное contract decision, которое agent не должен принимать молча.
Шаг 5. Рефакторьте на green
Refactoring меняет структуру без изменения observable behavior. Дайте agent ограниченную цель:
Retry tests green.
Отрефактори только src/retry/retry-delay.ts,
чтобы validation читалась проще.
Не меняй public behavior и tests.
После edit запусти narrow test, затем full test command.
Покажи diff и результаты обеих команд.
Refactor step не обязателен. Не добавляйте abstraction только потому, что у cycle есть соответствующая клетка. Убирайте реальное duplication или проясняйте существующее design pressure.
Запустите format, types, lint, build и integration checks, заданные repository. Unit suite не доказывает, что package компилируется или exported API остался совместимым.
Сохраните независимость test oracle
AI-assisted TDD замыкается в круг, когда один vague prompt порождает requirement, test и implementation.
Нужен хотя бы один независимый source:
- protocol или public API contract;
- принятый bug report с reproduction;
- domain example, одобренный subject-matter expert;
- database constraint;
- записанный request/response fixture;
- reference implementation;
- вручную рассчитанный example.
Затем разделите роли:
Contract review:
Найди ambiguity и missing decisions. Не пиши code.
Test author:
Используй approved contract и existing test style.
Не изучай и не редактируй planned implementation.
Implementer:
Используй approved test. Не изменяй его.
Challenger:
Изучи contract, tests и implementation.
Предложи минимальный counterexample, который нарушает contract,
но проходит текущий suite.
Это могут быть разные sessions или явные phases. Отдельные sessions уменьшают общие assumptions, но не создают ground truth. Источником остаётся contract.
Для чувствительных calculations проверьте несколько examples вручную. Для parsers храните accepted и rejected fixtures. Для state machines перечислите transitions. Security boundaries дополняйте AI code-review checklist и threat analysis.
Измеряйте силу tests, а не только coverage
Line и branch coverage показывают, какой code исполнился. Они не доказывают, что assertions ловят неверный result.
Проверяйте важные tests по возрастанию стоимости:
- Known-bad edit: временно разверните comparison или удалите validation. Test должен упасть.
- Boundary table: значения сразу ниже, на и выше каждой границы.
- Invariant: output остаётся в заявленном диапазоне; operation сохраняет idempotency, где это требуется.
- Mutation testing: автоматическое внесение маленьких faults и отчёт о surviving mutations.
- Real defect replay: regression test для каждого релевантного production bug.
Исследование 2024 года по LLM test generation использовало mutation feedback для улучшения fault-revealing ability вместо одного coverage (Dakhel et al.). Это не означает, что произвольный generated suite повторит score из paper или перенесётся на любой language и codebase.
Если mutation tool уже одобрен, сначала запускайте его на изменённом module. Surviving mutants — повод для review, но не автоматическое требование дописать test: существуют equivalent mutants и нерелевантные implementation details.
Не замокайте path, который нужно доказать
Test может быть быстрым и бесполезным:
billingClient.charge.mockResolvedValue({ status: 'paid' });
expect(billingClient.charge).toHaveBeenCalled();
Он доказывает, что mock вернул настроенное значение. Но ничего не говорит о webhook signature, idempotency key, database transaction или provider mapping.
Размещайте evidence на границе риска:
| Risk | Подходящие evidence |
|---|---|
| Pure calculation | unit examples + invariants |
| SQL mapping | repository integration test с реальной test database |
| External payload | contract fixture + schema validation |
| Auth policy | request-level test с representative identities |
| UI workflow | component test плюс небольшой critical E2E path |
| Agent output | deterministic validators, scenario evals и human review |
Для самих AI systems нужен отдельный agent evaluation plan. Probabilistic LLM judge в unit test требует versioning, thresholds, cost controls и calibration; это не готовый Boolean oracle.
Закрепите правило в repository
Prompts исчезают. Запишите workflow в project guidance или reusable command:
Для behavior changes:
1. подтвердить contract и test command;
2. добавить один failing test;
3. запустить и показать ожидаемый failure;
4. не редактировать production code до approval red;
5. внести минимальное production change;
6. не менять test ради green;
7. запустить narrow и full verification;
8. проверить diff до commit.
CI должен повторить authoritative checks в clean environment. Local agent transcript — полезные evidence, но не release gate. Подключите suite к существующему CI pipeline с теми же командами, которые developers запускают локально.
Когда AI-assisted TDD не подходит
Не навязывайте loop, если:
- team исследует неизвестное interaction и пока не определила behavior;
- outcome преимущественно visual и stable reference отсутствует;
- one-off migration лучше проверяется reconciliation;
- у legacy code нет seam для meaningful test без большого characterization effort;
- нужный oracle требует domain decision, которого никто не принял.
Сначала сделайте spike, соберите examples или напишите characterization tests. Throwaway code удалите либо явно отделите от production work. TDD работает, когда следующий behavior можно сформулировать и наблюдать; AI это требование не отменяет.
Короткая последовательность prompts для Claude Code
1. Прочитай [contract] и [existing test examples].
Предложи ordered test list. Не редактируй files.
2. Добавь ровно следующий test в [file].
Не редактируй production code.
Запусти [narrow command] и докажи expected red.
3. Внеси минимальное production change.
Не меняй tests.
Запусти [narrow command], затем [full command].
4. Проверь diff относительно contract.
Найди способ нарушить contract так,
чтобы текущие tests оставались green.
5. Если structure требует улучшения, refactor на green.
Запусти format, types, tests и build.
Сила TDD с coding agent не в более быстрой генерации tests. Она в видимом feedback loop: один approved behavior, один meaningful red, одно bounded green change и независимое доказательство, что test способен поймать fault.