# TDD с AI coding agents: как не обмануть себя тестами

> Практический red-green-refactor workflow для Claude Code и других coding agents: contract-first prompts, доказательство red, review тестов, mutations и CI.
> Author: Roman Belov · Published: 2026-04-10 · Source: https://futurecraft.pro/ru/blog/tdd-with-ai/

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.

1. написать следующий test для одного behavior;
2. запустить и увидеть ожидаемый failure;
3. написать достаточно production code для прохождения;
4. прогнать релевантный suite;
5. улучшить структуру без изменения поведения.

В описании TDD Martin Fowler сначала составляет test-case list, а затем выбирает **один** test на цикл. Последовательность tests направляет design ([Fowler](https://martinfowler.com/bliki/TestDrivenDevelopment.html)). Просьба сгенерировать двадцать failing tests, а затем реализовать всё сразу — test-first batch development, но не короткий TDD loop.

AI полезен тем, что быстро читает local conventions, редактирует, запускает команды и реагирует на failures. Актуальная документация Claude Code рекомендует давать конкретный context и verification target; agentic loop использует tests для проверки работы ([best practices](https://code.claude.com/docs/en/best-practices), [how Claude Code works](https://code.claude.com/docs/en/how-claude-code-works)).

Agent всё равно нужен oracle: human-approved contract, example, protocol или существующее поведение.

## Шаг 0. Сделайте environment детерминированным

До первого red-green cycle запишите:

```text
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:

```text
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, а не пересказывает его по памяти:

```text
Прочитай существующие 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:

```text
Добавь только первый 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:

```typescript
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);
  });
});
```

Проверьте три вещи:

1. test падает до production change;
2. причина — отсутствующее behavior;
3. failure исчезнет только при изменении observable behavior.

`Module not found` допустим для самого первого slice, но syntax error, broken fixture или отсутствующая test dependency не доказывают product behavior. Если test сразу проходит, разберитесь. Не изображайте состоявшийся red step.

Для bugfix сначала воспроизведите user-visible defect:

```text
Добавь один regression test, который падает на текущей branch
и представляет:
[observed input, state и expected output].

Не редактируй production code.
Пока не обобщай за пределы incident.
Запусти narrow test и покажи failure.
```

## Шаг 3. Внесите минимальное green change

После review red:

```text
Внеси минимальное production change, проходящее новый test.

Constraints:
- не изменяй, не удаляй, не skip и не ослабляй tests;
- не добавляй незапрошенное behavior;
- сохрани public API в approved contract;
- запусти narrow test, затем существующий retry suite;
- остановись и объясни, если сам test кажется неправильным.
```

Минимальная implementation намеренно мала:

```typescript
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 за цикл:

```typescript
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:

```typescript
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 ограниченную цель:

```text
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.

Затем разделите роли:

```text
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](/ru/blog/ai-code-review-checklist/) и threat analysis.

## Измеряйте силу tests, а не только coverage

Line и branch coverage показывают, какой code исполнился. Они не доказывают, что assertions ловят неверный result.

Проверяйте важные tests по возрастанию стоимости:

1. **Known-bad edit:** временно разверните comparison или удалите validation. Test должен упасть.
2. **Boundary table:** значения сразу ниже, на и выше каждой границы.
3. **Invariant:** output остаётся в заявленном диапазоне; operation сохраняет idempotency, где это требуется.
4. **Mutation testing:** автоматическое внесение маленьких faults и отчёт о surviving mutations.
5. **Real defect replay:** regression test для каждого релевантного production bug.

Исследование 2024 года по LLM test generation использовало mutation feedback для улучшения fault-revealing ability вместо одного coverage ([Dakhel et al.](https://doi.org/10.1016/j.infsof.2024.107468)). Это не означает, что произвольный generated suite повторит score из paper или перенесётся на любой language и codebase.

Если mutation tool уже одобрен, сначала запускайте его на изменённом module. Surviving mutants — повод для review, но не автоматическое требование дописать test: существуют equivalent mutants и нерелевантные implementation details.

## Не замокайте path, который нужно доказать

Test может быть быстрым и бесполезным:

```typescript
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](/ru/blog/ai-agent-testing-evaluation/). Probabilistic LLM judge в unit test требует versioning, thresholds, cost controls и calibration; это не готовый Boolean oracle.

## Закрепите правило в repository

Prompts исчезают. Запишите workflow в project guidance или reusable command:

```text
Для 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](/ru/blog/cicd-pipeline-generator/) с теми же командами, которые 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

```text
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.

## Источники

- [Martin Fowler: Test Driven Development](https://martinfowler.com/bliki/TestDrivenDevelopment.html)
- [UK Government Digital Service: TDD standard](https://gds-way.digital.cabinet-office.gov.uk/standards/test-driven-development.html)
- [Claude Code: best practices](https://code.claude.com/docs/en/best-practices)
- [Claude Code: common testing workflows](https://code.claude.com/docs/en/common-workflows)
- [Claude Code: how the agentic loop verifies work](https://code.claude.com/docs/en/how-claude-code-works)
- [Dakhel et al.: LLM test generation with mutation feedback](https://doi.org/10.1016/j.infsof.2024.107468)
