Programming on Mars
/
TDDAI-Assisted DevelopmentLLMTest PyramidJavaSpring BootClaude Code

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

LLMs sao nao-deterministicos. Testes sao o unico guardrail. Como TDD se torna o protocolo que transforma codigo gerado por AI de lixo em production-ready — e o step de validacao que a maioria das equipes pula.

André Lucas

March 19, 2026

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

TDD no Ciclo AI-Assisted

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 →

Tags

TDDAI-Assisted DevelopmentLLMTest PyramidJavaSpring BootClaude Code
← Back to home