TDD no Ciclo AI-Assisted: O Novo Step que Ninguem Documenta

No mes passado pedi ao Claude Code para implementar um use case de cancelamento de pedido. Ele gerou o controller, o service, a chamada ao repositorio. Codigo limpo, nomes razoaveis, ate um DTO. Zero testes. Zero edge cases. Nenhuma validacao para cancelar um pedido ja cancelado. Nenhuma verificacao de customerId nulo. O codigo compilava. Passaria num code review numa segunda-feira de manha. E quebraria em producao na terca.
O problema nao era o modelo. O problema era eu. Nao dei nenhuma estrutura sobre o que deveria falhar. Descrevi o happy path e esperei que o agente inferisse os unhappy paths. Ele nao consegue. Nenhum LLM consegue. Sao nao-deterministicos por design — o mesmo prompt produz codigo diferente em execucoes diferentes. Sem constraints explicitos, o output e uma moeda jogada para cima vestida de sintaxe limpa.
O Contexto: Por que "Basta Usar AI" Falha em Escala
Trabalho com Java e Spring Boot em sistemas que processam mais de um milhao de transacoes por dia. Nessa escala, "eventualmente consistente" nao e um conforto — e um risco que voce gerencia com contratos explicitos. Quando comecei a usar AI coding assistants em tempo integral no ano passado, a primeira coisa que quebrou nao foi minha arquitetura. Foi meu feedback loop.
No TDD classico, o feedback loop e curto: Red, Green, Refactor. Voce escreve um teste que falha, escreve o codigo minimo para passar, limpa. O developer segura tanto o conhecimento de dominio quanto a implementacao. Com AI no loop, esse ciclo se divide. O developer segura o conhecimento de dominio. A AI segura a velocidade de implementacao. Mas ninguem definiu o protocolo de handoff entre os dois. A maioria das equipes simplesmente escreve um prompt e torce pelo melhor. Isso nao e engenharia. E improvisacao.
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#21242D", "primaryTextColor": "#ffffff", "primaryBorderColor": "#FF7F50", "lineColor": "#BCC3D7", "background": "#0E0C15", "mainBkg": "#21242D", "fontFamily": "Sora, monospace"}}}%%
flowchart LR
subgraph CLASSIC[" CLASSICAL TDD "]
direction TB
D1(["Developer"]) -->|"writes failing test"| R["RED\ntest fails"]
R -->|"min. code to pass"| G["GREEN\ntest passes"]
G -->|"clean up"| RF["REFACTOR"]
RF -->|"next behaviour"| R
end
subgraph BROKEN[" TDD + AI — BROKEN LOOP "]
direction TB
D2(["Developer"]) -->|"describes feature"| AIGEN["AI generates\ncode + tests"]
AIGEN -->|"no review"| SKIPNODE{{"VALIDATION\nSKIPPED"}}
SKIPNODE -->|"ships directly"| PUSH["code pushed"]
PUSH -.->|"missing edge cases\nnull checks\nboundary conditions"| BUST["PROD BUG"]
end
CLASSIC ~~~ BROKEN
style R fill:#CC2222,color:#fff,stroke:#FF4444
style G fill:#2A6B3E,color:#fff,stroke:#3EB75E
style RF fill:#0F4F6E,color:#fff,stroke:#1BA2DB
style SKIPNODE fill:#7A3A00,color:#FFC876,stroke:#FF7F50,stroke-width:2px
style BUST fill:#3D0000,color:#FF7F50,stroke:#CC5200,stroke-width:2px
style CLASSIC fill:#16181E,stroke:#2E313D,color:#BCC3D7
style BROKEN fill:#16181E,stroke:#FF7F50,color:#BCC3D7
O Novo Ciclo: TDD como Protocolo de Comunicacao para LLMs
Depois de meses pareando diariamente com Claude Code, convergi para um ciclo que funciona. Nao e revolucionario — e TDD com um step explicito que a maioria das equipes pula. Aqui esta o loop completo:
Step 1 — Developer define cenarios de dominio. Nao codigo. Nao pseudocodigo. Cenarios. Happy flow, unhappy flow, edge cases, condicoes de contorno. Escritos em linguagem natural ou prompts estruturados. Aqui mora o conhecimento de dominio, e e exatamente o que a maioria dos developers pula quando prompta uma AI.
Step 2 — AI gera a piramide de testes. Testes unitarios para logica de dominio. Testes de integracao para persistencia e transacoes. Testes E2E para contratos de API. A AI nao decide o que testar — voce ja disse. A AI decide como expressar isso em codigo.
Step 3 — Developer valida os testes gerados. Esse e o step que ninguem documenta. Voce le cada teste que a AI gerou. Voce verifica: capturou o edge case em que um pedido com zero itens deveria lancar uma BusinessException? Testou o que acontece quando voce cancela um pedido ja cancelado? Cobriu o contrato — o HTTP 400 quando customerId e nulo? Se a resposta e nao, voce corrige os cenarios e volta ao Step 2.
Step 4 — AI implementa o codigo de producao. So apos os testes serem validados. A AI agora tem uma especificacao concreta e executavel do que significa "correto". Nao esta adivinhando. Esta resolvendo um problema de satisfacao de constraints.
Step 5 — AI roda os testes como guardrail a cada mudanca. Cada tentativa de implementacao e validada contra a suite de testes automaticamente. Testes falham, AI ajusta. Testes passam, iteracao concluida. O developer nao esta revisando codigo gerado linha por linha — os testes estao fazendo a revisao.
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#21242D", "primaryTextColor": "#ffffff", "primaryBorderColor": "#FF7F50", "lineColor": "#BCC3D7", "background": "#0E0C15", "mainBkg": "#21242D", "clusterBkg": "#16181E", "fontFamily": "Sora, monospace"}}}%%
flowchart TD
DEV(["DEVELOPER\ndomain expert"])
AI(["AI\nimplementation engine"])
S1["STEP 1\nDefine domain scenarios\nhappy path · edge cases\nboundary conditions · failures"]
S2["STEP 2\nGenerate test pyramid\nUnit · Integration · E2E"]
S3{{"STEP 3\nValidate generated tests\nagainst domain reality"}}
S4["STEP 4\nImplement production code\nwith tests as spec"]
S5["STEP 5\nRun tests as guardrail\non every iteration"]
FAIL_LOOP["revise scenarios\n+ regenerate"]
DONE(["tests GREEN\niteration complete"])
DEV -->|"structured prompt\nwith constraints"| S1
S1 --> AI
AI -->|"unit + integration + e2e"| S2
S2 --> DEV
DEV -->|"reads every test"| S3
S3 -->|"tests capture\nedge cases correctly"| S4
S3 -->|"tests miss domain\nrules or edge cases"| FAIL_LOOP
FAIL_LOOP -->|"back to step 1"| S1
S4 --> AI
AI -->|"implements against spec"| S5
S5 -->|"tests FAIL\nAI adjusts"| S4
S5 -->|"tests PASS"| DONE
style DEV fill:#16181E,stroke:#FF7F50,color:#FF7F50,stroke-width:2px
style AI fill:#16181E,stroke:#1BA2DB,color:#1BA2DB,stroke-width:2px
style S1 fill:#21242D,stroke:#2E313D,color:#BCC3D7
style S2 fill:#21242D,stroke:#2E313D,color:#BCC3D7
style S3 fill:#7A3A00,stroke:#FF7F50,color:#FFC876,stroke-width:2px
style S4 fill:#21242D,stroke:#2E313D,color:#BCC3D7
style S5 fill:#21242D,stroke:#3EB75E,color:#3EB75E
style FAIL_LOOP fill:#3D0000,stroke:#CC5200,color:#FFA07A
style DONE fill:#0F3D1F,stroke:#3EB75E,color:#3EB75E
A Piramide de Testes como Mapa de Contexto
O insight que mudou como eu trabalho com AI: cada camada da piramide de testes mapeia para uma camada diferente de contexto que o LLM precisa.
Testes unitarios codificam regras de dominio. Quando digo a AI "um pedido com itens vazios deve lancar BusinessException", estou dando um constraint de dominio. O teste unitario e a forma executavel desse constraint:
@Test
void shouldThrowWhenItemsAreEmpty() {
assertThatThrownBy(() -> Order.create(UUID.randomUUID(), Set.of()))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("items cannot be empty");
}
@Test
void shouldThrowWhenCustomerIdIsNull() {
var items = Set.of(new OrderItem(UUID.randomUUID(), 1, new BigDecimal("10.00")));
assertThatThrownBy(() -> Order.create(null, items))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("customerId cannot be null");
}
Esses testes existem no mars-enterprise-kit-lite. Testam Order.create() — um factory method statico num Java record. Sem contexto Spring, sem banco de dados, sem Kafka. Logica de dominio pura. A AI roda centenas desses em milissegundos. Cada um e um constraint que a implementacao precisa satisfazer.
Testes de integracao codificam contratos de infraestrutura. Aqui vivem transacoes, mapeamentos de persistencia e constraints de banco:
class CreateOrderUseCaseIntegrationTest extends AbstractIntegrationTest {
@Autowired
private CreateOrderUseCase createOrderUseCase;
@Autowired
private OrderJpaRepository orderJpaRepository;
@Test
@DisplayName("should create order and persist in database")
void shouldCreateOrderAndPersistInDatabase() {
var items = Set.of(new OrderItem(UUID.randomUUID(), 2, new BigDecimal("10.00")));
var customerId = UUID.randomUUID();
var orderId = createOrderUseCase.execute(
new CreateOrderUseCase.Input(items, customerId));
assertThat(orderId).isNotNull();
var entity = orderJpaRepository.findById(orderId).orElseThrow();
assertThat(entity.getCustomerId()).isEqualTo(customerId);
assertThat(entity.getStatus()).isEqualTo(OrderStatus.CREATED);
assertThat(entity.getTotal()).isEqualByComparingTo(new BigDecimal("20.00"));
}
}
Esse teste estende AbstractIntegrationTest, que sobe um container PostgreSQL real via Testcontainers. A AI sabe: tem um banco real, migracoes Flyway precisam rodar, o mapeamento JPA precisa estar correto, o total precisa ser calculado como quantidade vezes preco unitario. Esse e contexto de infraestrutura que a AI nao consegue inferir so de um prompt.
Testes E2E codificam contratos de API. Qual status code para um array de itens vazio? Qual formato de resposta para uma criacao bem-sucedida?
@Test
@DisplayName("POST /orders - should return 400 when items are empty")
void shouldReturn400WhenItemsAreEmpty() {
given()
.contentType(ContentType.JSON)
.body("""
{
"customerId": "550e8400-e29b-41d4-a716-446655440000",
"items": []
}
""")
.when()
.post()
.then()
.statusCode(400);
}
REST Assured com assercoes no estilo BDD. A AI le isso e sabe: itens vazios e 400, nao 500. Isso e um contrato. O developer definiu. A AI implementa.
Se quiser ver esse padrao em acao, o Mars Enterprise Kit Lite implementa uma infraestrutura de testes production-ready com testes unitarios, testes de integracao com Testcontainers e E2E com REST Assured — exatamente o setup descrito nesse ciclo. Veja a implementacao →
O Que Deu Errado: O Step de Validacao que Quase Pulei
Na primeira vez que tentei esse ciclo, pulei o Step 3. Deixei a AI gerar testes e fui direto para implementacao. A AI produziu 12 testes unitarios para o aggregate Order — todos passando, todos verdes. Me senti produtivo. Ai olhei o que realmente estava sendo testado: 8 dos 12 testes eram variacoes do happy path. Nenhum teste para cancelar um pedido ja cancelado. Nenhum teste para customerId nulo. Nenhum teste para quantidades negativas.
A AI fez exatamente o que pedi — gerou testes. Mas otimizou para porcentagem de cobertura, nao para corretude de dominio. Um humano que entende o dominio de negocios nunca escreveria 8 variacoes de happy path e zero testes de edge case. Esse e o gap. A AI nao sabe o que importa a menos que voce diga.
Depois disso, o step de validacao se tornou inegociavel. Hoje gasto mais tempo revisando testes gerados do que revisando codigo de implementacao gerado. Se os testes estao certos, a implementacao segue. Se os testes estao errados, nenhuma qualidade de implementacao salva.
Trade-offs: Quando Esse Ciclo Nao Funciona
Esse workflow assume que o developer tem conhecimento profundo de dominio. Se voce esta construindo um CRUD sem regras de negocio, o overhead de definir cenarios e validar testes nao compensa — basta promptar e mandar.
Tambem assume que sua infraestrutura de testes e solida. Testcontainers, setup de testes de integracao decente, feedback loops rapidos. Se sua suite de testes leva 20 minutos para rodar, o loop com AI vira gargalo em vez de acelerador. No mars-enterprise-kit-lite, a suite completa — unitarios, integracao e E2E — roda em menos de 30 segundos por causa de Testcontainers compartilhados e escopo de teste focado.
E existe um requisito de senioridade que ninguem menciona. Um developer que nunca praticou TDD nao consegue definir cenarios significativos para a AI. Vai descrever happy paths, a AI vai gerar testes de happy path, e os bugs de producao vao vir dos edge cases que nenhum dos dois considerou. O novo ciclo nao baixa a barra de habilidade de engenharia — ele levanta.
O Takeaway
TDD nao morreu na era AI. E o protocolo. O developer que consegue expressar fronteiras de dominio, edge cases e modos de falha em cenarios estruturados vai obter codigo production-ready de qualquer LLM. O developer que nao consegue vai obter codigo que compila e falha em producao.
O step que faz a diferenca e o que ninguem documenta: valide os testes gerados antes de deixar a AI implementar qualquer coisa. E ai que expertise de dominio ganha seu espaco.
Se voce quer entender como isso se conecta com a questao mais ampla de estruturar projetos para agentes de AI, escrevi sobre isso em AI-First Software Design: Beyond CLAUDE.md — a context engineering que viabiliza esse ciclo de TDD.
A versao Lite entrega a infraestrutura de testes: testes unitarios de dominio, testes de integracao com Testcontainers e contratos E2E com REST Assured — tudo que voce precisa para rodar esse ciclo no seu proprio codebase. A versao PRO adiciona a implementacao de referencia completa com Event Sourcing, orquestracao de SAGA e o workflow de AI-assisted development ja aplicado end-to-end em tres servicos production-grade. Entre na early access →