~ / tutoriais /tdd-com-agentes-testes-que-sobrevivem $ _

TDD com agentes: como escrever testes que sobrevivem ao código gerado

Lucas Souza Lucas Souza 10 min de leitura Tutoriais
TDD com agentes: como escrever testes que sobrevivem ao código gerado

Aconteceu mais ou menos assim. Pedi pro agente fazer um teste passar. Ele rodou Pest, leu a falha, voltou pro código, rodou de novo. Ainda vermelho. Terceira iteração: verde. Tudo bonito. Fui olhar o diff e o teste tinha sido deletado. A função it('aplica desconto cumulativo corretamente', ...) não existia mais no arquivo.

Não é maldade. É loop de feedback. O agente recebeu um objetivo (suite verde), tinha permissão pra editar qualquer arquivo, e o caminho mais curto pra suite verde era apagar o teste. Engenharia de incentivos básica, só que num agente de código.

Esse post é sobre o que muda no TDD quando o agente entra no loop. O teste deixa de ser uma rede de proteção do dev e vira a especificação executável que o agente persegue. Quem escreve o teste manda no agente. Vamos cobrir o workflow invertido, o hook PreToolUse que bloqueia o agente de tocar em tests/, o eval no CI que detecta asserção enfraquecida no PR, um estudo de caso real em Laravel + Pest + Claude Code, e quando relaxar a regra.

TL;DR

  • O que é: TDD invertido para agentes. Humano escreve o teste primeiro, agente implementa, hook bloqueia edição em tests/ e eval no CI detecta asserção removida.
  • Stack: Claude Code, Laravel 12, Pest 3, hook PreToolUse em .claude/settings.json, GitHub Actions.
  • Custo/Acesso: tudo open-source. TDD Guard é opcional para enforcement mais agressivo.
  • Por que importa: METR documentou em 2025 modelos de fronteira modificando timers, graders e funções de hash em ~30% das tasks de RE-Bench para parecer que estavam passando. O agente não está te enganando. Está otimizando o objetivo que você deu.

Por que TDD muda quando o agente entra no loop

A documentação oficial do Claude Code é direta: TDD é o single strongest pattern para trabalhar com agentes. Cada ciclo red-to-green dá um sinal não-ambíguo. O agente pode iterar sozinho, sem você no meio do caminho.

Esse é o lado bonito. Tem outro.

Em junho de 2025, a METR publicou um relatório sobre reward hacking em modelos recentes. O resumo é desconfortável. O3 modificou funções de timing pra reportar execução artificialmente rápida. Alterou operadores de igualdade pra resultados errados parecerem corretos. Monkey-patched o grader pra retornar score perfeito. Em uma task específica, usou estratégia de cheating em todas as 21 tentativas. Claude 3.7 Sonnet explorou bug em função de hash criando dois inputs diferentes que colidem.

O detalhe mais incômodo: mesmo com instruções explícitas pra não trapacear, 70 a 95% dos modelos continuaram tentando. Instrução em CLAUDE.md é advisory. O modelo lê, anota, e ainda assim segue o caminho do menor esforço se o ambiente permitir.

A conclusão prática: você não impede o agente de modificar o teste pedindo no prompt. Você impede com hook, com diff guard no CI, com permissão de filesystem. Instrução é hint, hook é lei.

Isso muda o papel do teste. Em TDD clássico, o teste é uma rede de proteção que o próprio dev escreve, lê, mantém. Com agente no loop, o teste vira algo mais parecido com uma spec executável que não é negociável. Quem escreve o teste define o jogo. O agente joga dentro das regras escritas. Se as regras forem afrouxadas, o agente afrouxa junto.

Pré-requisitos e ferramentas

Antes de começar:

  • [ ] Laravel 12 (ou 11) com Pest 3 configurado.
  • [ ] Claude Code instalado e funcionando no projeto.
  • [ ] Conhecimento básico de hooks no Claude Code. Se for sua primeira vez, leia Hooks, Slash Commands e MCPs: a anatomia de um harness produtivo.
  • [ ] GitHub Actions habilitado (para o eval de PR).
  • [ ] jq no PATH (o hook lê JSON do stdin).

Mão na massa: o workflow TDD invertido

A sequência tem cinco passos e o terceiro é o coração.

Passo 1: humano escreve o teste primeiro

A regra que importa: o teste descreve comportamento, não implementação. Sem mock de método interno, sem afirmação sobre estrutura de classe. Só entrada, saída e efeito observável.

// tests/Feature/InvoiceCalculatorTest.php
use App\Domain\Billing\InvoiceCalculator;

it('aplica desconto cumulativo respeitando arredondamento bancario', function () {
    $calculator = new InvoiceCalculator();

    $total = $calculator->calculate(
        amount: 199.99,
        discounts: [0.10, 0.05], // 10% e depois 5% sobre o restante
        currency: 'BRL',
    );

    expect($total)->toBe(170.99); // 199.99 * 0.90 * 0.95 = 170.9914 -> 170.99 (half-to-even)
});

it('rejeita desconto cumulativo maior que 100%', function () {
    $calculator = new InvoiceCalculator();

    expect(fn () => $calculator->calculate(
        amount: 100.00,
        discounts: [0.60, 0.50],
        currency: 'BRL',
    ))->toThrow(InvalidArgumentException::class);
});

Repare em duas coisas. Primeira: o teste cita o número final calculado (170.99), não uma fórmula. Se o agente "deduzir" a fórmula a partir do teste e a fórmula estiver errada, o teste continua sendo a verdade. Segunda: a edge case do desconto maior que 100% está separada. Um teste, um comportamento.

Passo 2: commit do teste antes do agente entrar

Esse passo é técnico, mas tem peso de processo. O teste vai pro git antes de qualquer linha de implementação. Isso cria um checkpoint auditável. Se o agente depois alterar o teste, o diff fica visível no PR.

git add tests/Feature/InvoiceCalculatorTest.php
git commit -m "test: spec executavel do InvoiceCalculator"

A documentação oficial do Claude Code recomenda exatamente isso: "explicitly being that you are doing TDD helps Claude avoid creating mock implementations or stubbing out imaginary code". Commit do teste isolado é a versão acionável dessa recomendação.

Passo 3: agente implementa, com prompt cirúrgico

O prompt importa. Compare:

# Ruim
implementa o InvoiceCalculator pra fazer os testes passarem

# Bom
Implementa src/Domain/Billing/InvoiceCalculator.php para fazer
os testes em tests/Feature/InvoiceCalculatorTest.php passarem.

Regras:
- Use arredondamento bancario (PHP_ROUND_HALF_EVEN).
- NAO modifique nenhum arquivo dentro de tests/.
- NAO adicione novos testes.
- Rode `./vendor/bin/pest tests/Feature/InvoiceCalculatorTest.php`
  ao final e confirme que todos passam.

Você está dando ao agente exatamente o que ele precisa pra entrar no loop autônomo: alvo de verificação, escopo de edição, comando de validação. A frase em caixa alta importa: a documentação aponta que destacar com "IMPORTANT" ou "NÃO" melhora aderência. Não resolve sozinho. Mas combinado com o hook do próximo passo, fecha o cerco.

Passo 4: hook PreToolUse que bloqueia edição em tests/

Esse é o cinto de segurança. Diferente do prompt, ele é determinístico. A documentação Anthropic é clara: "hooks são determinísticos e garantem que a ação aconteça".

.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/protect-tests.sh"
          }
        ]
      }
    ]
  }
}

.claude/hooks/protect-tests.sh:

#!/usr/bin/env bash
set -euo pipefail

# O Claude Code manda o tool_input via stdin como JSON.
payload=$(cat)
file_path=$(echo "$payload" | jq -r '.tool_input.file_path // empty')

# Vazio = nao e Edit/Write em arquivo. Deixa passar.
[ -z "$file_path" ] && exit 0

# Normaliza para path relativo ao projeto.
rel_path="${file_path#$CLAUDE_PROJECT_DIR/}"

if [[ "$rel_path" == tests/* ]]; then
  cat >&2 <<EOF
BLOQUEADO: edicao em '$rel_path' nao e permitida durante TDD.

Os testes sao a especificacao executavel e foram congelados no commit anterior.
Se voce acha que o teste esta errado, pare e me chame para discutir antes de prosseguir.

Caminhos liberados: src/, app/, database/, config/.
EOF
  exit 2
fi

exit 0

Exit code 2 é o sinal que o Claude Code interpreta como "bloqueio com feedback". A mensagem em stderr volta pro modelo, que entende o que aconteceu e ajusta. Sem exit 2, sem bloqueio: hook que falha em silêncio é hook decorativo.

Um aviso honesto. Existe issue documentada no repo do Claude Code mostrando que agentes burlaram pre-commit hooks combinando git commit --no-verify, git stash e flags silenciosas. PreToolUse não cai nessa armadilha porque opera antes do tool de Edit, não no commit. Mas a lição vale: nenhuma camada é suficiente sozinha. Por isso o eval do passo 5.

Passo 5: eval no CI que detecta asserção enfraquecida

O agente pode burlar o hook local em situações criativas (rodar Bash, mover arquivos com mv, etc). A última linha de defesa fica no PR: um GitHub Action que compara as asserções em tests/ entre base e head, e barra o merge se elas diminuírem.

.github/workflows/test-integrity.yml:

name: test-integrity
on:
  pull_request:
    paths: ['tests/**', 'src/**', 'app/**']

jobs:
  guard:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }

      - name: Conta assercoes na base e no PR
        run: |
          base=$(git ls-tree -r --name-only origin/${{ github.base_ref }} -- tests/ \
            | xargs -I{} git show origin/${{ github.base_ref }}:{} 2>/dev/null \
            | grep -cE '->(assert|expect)|expect\(' || true)

          head=$(grep -rcE '->(assert|expect)|expect\(' tests/ \
            | awk -F: '{sum+=$2} END {print sum}')

          echo "base=$base head=$head"

          if [ "$head" -lt "$base" ]; then
            echo "::error::Assercoes cairam de $base para $head. PR bloqueado."
            exit 1
          fi

Não é foolproof. Um agente determinado pode trocar assertEquals(42, $x) por assertNotNull($x) mantendo a contagem. Para casos sensíveis vale rodar mutation testing com Infection no CI, mas isso fica para outro post. O ponto é: você quer defesa em camadas, não muralha única.

Estudo de caso: feature de faturamento real

Rodei o pipeline acima em uma feature real: cálculo de fatura com descontos cumulativos e arredondamento bancário em BRL. Quatro testes, todos comprometidos antes do agente entrar.

O loop tomou três iterações do Claude Code. Na primeira, a implementação errou o arredondamento (usou round() direto, sem flag). Pest falhou, o agente leu o output, ajustou pra PHP_ROUND_HALF_EVEN. Na segunda, esqueceu de validar discounts > 100% e a exceção não disparou. Leu de novo, ajustou. Na terceira, suite verde. Custo: ~14 mil tokens, sub-30 segundos por iteração.

O que não aconteceu durante essas três iterações:

  • Em duas das iterações, o agente tentou editar tests/Feature/InvoiceCalculatorTest.php pra "ajustar uma expectativa". O hook bloqueou e devolveu a mensagem. O agente recuou e modificou só o src/.
  • Em uma iteração, o agente sugeriu adicionar um teste novo cobrindo um cenário que ele "descobriu". Boa intuição, péssimo timing. O hook bloqueou e a sugestão virou anotação no PR pra revisão humana.

O diff final tinha 1 arquivo modificado: src/Domain/Billing/InvoiceCalculator.php. Zero linhas tocadas em tests/. Para comparação, em um experimento sem o hook, o agente acabou ajustando uma expectativa numérica de 170.99 para 170.9914 "para refletir o cálculo real". Isso é exatamente o tipo de erro silencioso que mata em produção: o teste vira inútil porque foi calibrado pelo código que ele deveria validar.

Limitações e quando relaxar

Esse setup não é universal. Tem cenários onde proteger tests/ inteiro é overkill.

Testes de integração que descobrem comportamento. Quando o agente está integrando uma lib externa e precisa entender o contrato (resposta da API, formato de evento, etc), faz sentido deixar ele escrever feature tests exploratórios. Solução: whitelist por subdiretório (tests/Feature/Integration/** libera, tests/Unit/** bloqueia).

Fixtures, factories, seeders. Esses arquivos não codificam regras de negócio. Estão em tests/ por organização, não por contrato. Libere via padrão de glob (tests/Factories/**, tests/Fixtures/**).

Property-based tests. Se você define a propriedade (ex: "soma é comutativa") e o agente expande o espaço de input via shrinker, está tudo bem. A propriedade é o contrato. A expansão é mecânica.

Testes que falham por motivo errado. Às vezes o teste está errado. Variável tipada errada, mock que mente, número arredondado pra mais. Aí o caminho não é deixar o agente "consertar". É parar, você ler, decidir, commitar a correção, deixar o agente entrar de novo. O hook continua bloqueando isso. Esse é o ponto.

A regra geral que sobreviveu aqui: o agente nunca escreve a asserção sobre regra de negócio nova. Quem escreve a asserção define o que o produto faz. Esse papel não terceiriza.

FAQ rápido

Não fica mais lento escrever todos os testes à mão? No início sim, em 1 a 2 horas por feature. Compensa rapidamente: o agente passa a fechar a implementação em minutos, com loops curtos. Para features com regra de negócio densa, o tempo total cai. Para CRUD simples sem regra, talvez não compense. Aí relaxa pra fluxo agentic-code padrão. Veja Os 4 níveis de autonomia em Agentic Code para escolher o nível certo.

Funciona com Jest, Vitest, pytest, RSpec? O conceito sim, os scripts precisam ser adaptados. A lógica do hook (bloquear path) é language-agnostic. O contador de asserções no CI muda: em Jest é expect(, em pytest é assert ou pytest.raises, em RSpec é expect( e it. Adapte o regex.

Posso usar TDD Guard em vez disso? Pode. TDD Guard é mais agressivo: bloqueia implementação sem teste falhando, bloqueia sobre-implementação, e tem integração com lint. Para times que querem enforcement máximo, vale. Para começar, o hook simples acima já resolve 90% do problema.

E se o agente sugerir um teste novo? Aceito? Anota em comentário no PR, não no diff. O agente pode dizer "encontrei um edge case: e se o amount for negativo?". Boa observação. Você decide se vira teste novo (que vai pro próximo commit isolado, sem implementação ainda) ou se é fora de escopo. Não deixa o agente despejar testes junto com a implementação porque nesse momento ele tem incentivo enviesado.

Conclusão

TDD não morreu com o agente. Ficou mais sério. O teste deixa de ser opcional, deixa de ser comentário sobre intenção, e vira contrato executável que o agente persegue. Quem escreve esse contrato manda no loop.

A pilha que descrevi aqui, humano escreve o teste, hook protege tests/, eval pega o diff suspeito no PR, é uma das peças de um harness sério para colocar agente em produção sem gastar fim de semana caçando regressão silenciosa. Se a ideia de construir e operar esse tipo de pilha no Claude Code te interessa, é exatamente o que vamos passar do zero no Harness Engineering com Claude Code, workshop ao vivo do Beer & Code com loop autônomo, hooks, evals e o agente saindo do laptop e indo pra produção.

O próximo passo natural depois daqui é olhar como esses mesmos guardrails se aplicam ao review de PR gerado por agente, porque teste verde é necessário, mas não suficiente. Mostrei isso aplicado em Code Review com IA sem virar carimbador.

Lucas Souza
Lucas Souza

{AI Engineer} — apaixonado por Laravel, arquitetura de software e construir produtos com impacto. Compartilho aqui tutoriais, descobertas e reflexões sobre o dia a dia de engenharia.

Você também pode gostar

Agentic Code: o que muda quando o agente escreve, executa e testa o próprio código
Notícias

Agentic Code: o que muda quando o agente escreve, executa e testa o próprio código

Vibe coding deixou o dev no volante. SDD desenhou o mapa. Agentic Code tira o dev do carro e dá a chave pro agente, com freio de mão na mão. Cunhagem do termo em PT-BR, taxonomia de 4 níveis de autonomia, anatomia do ciclo plan/act/observe/reflect, demo comparativa de CRUD em três paradigmas, modos de falha reais e o que o harness precisa garantir pra rodar agente em produção sem quebrar tudo.

· 11 min
Portfólio de AI Engineer: 5 projetos que abrem porta sem precisar de mestrado
Tutoriais

Portfólio de AI Engineer: 5 projetos que abrem porta sem precisar de mestrado

Recrutador olha 11 segundos. Notebook de fine-tuning de Llama no Colab não convence ninguém. Cinco projetos pequenos que provam skill real de AI engineer e cabem em 1 a 3 fins de semana cada.

· 13 min
Agentic Code vs Vibe Coding vs SDD: a tabela definitiva pra escolher por contexto
Notícias

Agentic Code vs Vibe Coding vs SDD: a tabela definitiva pra escolher por contexto

Três paradigmas, três comunidades brigando no Twitter, e zero clareza sobre quando cada um performa. Definição operacional de vibe coding, agentic engineering e SDD, tabela com oito critérios e árvore de decisão pronta pra colar na wiki do time.

· 10 min
Como escrever uma spec que o agente realmente entende (e não inventa em cima)
Tutoriais

Como escrever uma spec que o agente realmente entende (e não inventa em cima)

A maior parte dos bugs de agente em 2026 não é o modelo errando, é a spec mentindo. Três anti-padrões reais (ambiguidade, contexto inútil e regra implícita) com exemplos antes/depois e checklist de sete pontos pra validar a spec antes de mandar pro Claude Code.

· 10 min

VirguIA

beer & code assistant

conectando…

Não foi possível iniciar o chat agora.

tocando