Testes unitários com IA: um guia prático

Como escrever testes unitários com agentes de IA que pegam bugs de verdade: testes de caracterização, lacunas de cobertura, mutation testing e dois agentes.

Solviera Teknoloji 12 min de leitura

Se você quer escrever testes unitários com IA, a parte fácil é conseguir testes. Peça a qualquer agente de código “testes para este módulo” e em poucos minutos você terá quarenta testes verdes. A parte difícil é conseguir testes que falhem quando o código está errado. Um agente que lê uma função e depois escreve asserções sobre o que essa função retorna, na maior parte das vezes, escreveu a função uma segunda vez.

Este guia mostra como tirar testes úteis de agentes de código como Claude Code, Codex ou Gemini CLI: o que pedir, como identificar testes que não provam nada, como medi-los com cobertura e mutation testing e como dividir o trabalho para que o agente que escreve o código não consiga entortar os testes em silêncio.

Por que testes escritos por IA dão errado

Agentes são muito bons nas partes mecânicas dos testes: fixtures, factories, mocks, casos parametrizados, o boilerplate de um arquivo de teste novo. Onde eles escorregam é justamente no que dá valor a um teste: o valor esperado.

Quando um agente escreve um teste, normalmente ele tem a implementação aberta no contexto. O movimento natural é ler o código, descobrir o que ele retorna e afirmar exatamente isso. Se o código tem um bug, o teste agora trava o bug no lugar. Está verde, cobre as linhas e continuaria verde se você nunca corrigisse o bug.

Isso aparece em alguns formatos comuns:

  • Lógica espelhada. O teste calcula o valor esperado com a mesma fórmula do código: expect(price(100, 0.2)).toBe(100 - 100 * 0.2). Se a fórmula está errada, os dois erram juntos.
  • Mock do próprio alvo. O que está sendo testado é mockado, então o teste verifica o mock.
  • Asserções só de chamada. expect(sendEmail).toHaveBeenCalled() sem checar o que foi enviado nem para quem.
  • Snapshot de tudo. Um snapshot enorme que ninguém lê, atualizado com -u sempre que falha.
  • Asserções que não podem falhar. expect(result).toBeDefined() numa função que sempre retorna um objeto.
  • Editar o teste para passar. O código falha num teste e o agente “corrige” o valor esperado do teste em vez do código.

Nada disso é exclusivo da IA. Pessoas também escrevem assim. Agentes só produzem isso mais rápido e em lotes maiores, então você precisa de um processo que pegue esses casos.

Comece pelo comportamento, não pelo código

A maior melhoria isolada é dizer ao agente o que o código deveria fazer e manter os valores esperados fora do alcance da implementação.

Um prompt fraco:

Escreva testes unitários para src/billing/discount.ts.

Um melhor:

Escreva testes unitários para applyDiscount em src/billing/discount.ts. Regras: um desconto percentual nunca leva o total abaixo de zero; descontos não se acumulam, vale o maior; um código expirado é ignorado, não é erro; valores são arredondados para 2 casas decimais, metade para cima. Escreva os valores esperados à mão a partir dessas regras, sem rodar nem ler a implementação. Se uma regra parecer discordar do código atual, escreva o teste mesmo assim e me diga quais testes falham.

Três coisas mudaram. As regras vêm de você, ou de um ticket, uma especificação ou a documentação. Os valores esperados são literais calculados a partir das regras. E um teste que falha é permitido: pode ser um bug real, que é exatamente para isso que servem os testes.

Mais alguns hábitos ajudam:

  • Peça a lista de casos primeiro. “Liste os casos que você pretende testar, um por linha, antes de escrever qualquer código.” Você pode acrescentar os casos-limite que ele esqueceu (entrada vazia, unicode, fusos horários, o limite exatamente em zero) antes de existir código.
  • Nomeie os limites. Agentes são bons em casos-limite quando você aponta a dimensão: “entradas exatamente no limite, logo abaixo e logo acima de cada um”.
  • Diga o que não mockar. “Não faça mock de nada dentro de src/billing; mocke só o cliente HTTP.” Excesso de mock é como muitos testes tautológicos começam.
  • Um comportamento por teste. Testes com um nome claro como ignores_expired_codes são mais fáceis de revisar do que um teste com doze asserções.

Testes unitários, de integração e end-to-end

O mesmo agente pode escrever os três tipos, mas as instruções mudam.

Testes unitários

Rápidos, isolados, muitos. É aqui que os agentes economizam mais tempo. Aponte o agente para o estilo de teste que você já usa (“siga tests/billing/tax.test.ts”) para não ganhar uma biblioteca de helpers nova a cada vez. Coloque o comando de teste no seu CLAUDE.md ou AGENTS.md para o agente rodar os testes sozinho; mais sobre isso em como escrever CLAUDE.md e AGENTS.md.

Testes de integração

Estes tocam um banco de dados, uma fila ou um sistema de arquivos reais. Aqui os agentes tendem a recorrer a mocks, o que transforma o teste de integração de volta em teste unitário. Seja explícito: “use o banco de testes do docker-compose.test.yml; não faça mock da camada de repositório.” Peça também a limpeza, para que os testes não dependam das sobras uns dos outros.

Testes end-to-end

Para testes de navegador, um agente pode escrever specs de Playwright ou Cypress a partir de um fluxo de usuário que você descreve. Os seletores são o ponto fraco: peça seletores baseados em role ou label (getByRole('button', { name: 'Pay' })) em vez de caminhos CSS que quebram no próximo redesign. O post testes de navegador com agentes de IA mostra como deixar um agente pilotar um navegador real para conferir o próprio trabalho.

Testes de caracterização para código que você ainda não entende

Às vezes não existe especificação. O código é antigo, quem escreveu já saiu e ninguém sabe exatamente o que ele faz. Aqui o objetivo se inverte: você quer testes que registrem o que o código faz hoje, bugs incluídos, para perceber quando isso mudar.

Esses são os testes de caracterização, e são um dos casos em que deixar o agente ler a implementação é a decisão certa. O prompt muda de acordo:

Escreva testes de caracterização para legacy/invoice_export.py. Registre o comportamento atual, incluindo qualquer coisa que pareça bug; não corrija nada. Cubra cada branch que encontrar. Marque qualquer comportamento que pareça errado com um comentário # SUSPICIOUS: explicando por quê.

As notas SUSPICIOUS viram sua lista de bugs para depois. Testes de snapshot ou de aprovação servem aqui, desde que sejam pequenos o bastante para ler. Esse é o primeiro passo de qualquer refatoração segura, que o guia refatorar código legado com IA percorre.

Use a cobertura para achar lacunas, não para dar nota aos testes

Cobertura é um bom mapa e uma má nota. Use-a para dizer ao agente onde olhar:

StackComando
Vitestnpx vitest run --coverage
Jestnpx jest --coverage
pytest (com pytest-cov)pytest --cov=myapp --cov-report=term-missing
Gogo test -coverprofile=cover.out ./... e depois go tool cover -func=cover.out

O Vitest precisa de um provedor de cobertura instalado, como @vitest/coverage-v8. O relatório term-missing do pytest lista os números exatos das linhas não cobertas, o que é prático para colar direto num prompt.

Um bom ciclo:

  1. Rode a cobertura e escolha os módulos que importam: cobrança, autenticação, migrações de dados, não código gerado.
  2. Entregue ao agente as linhas não cobertas e pergunte que comportamento elas representam.
  3. Peça testes para esse comportamento, com valores esperados vindos das regras.
  4. Rode a cobertura de novo para confirmar que as linhas agora são exercitadas.

O que você não deve fazer é dizer “leve a cobertura a 90%”. Um agente que recebe um número vai bater o número, e o jeito mais barato é com testes que executam linhas sem verificar muita coisa.

Mutation testing: os testes estão verificando alguma coisa?

O mutation testing responde à pergunta que a cobertura não responde. Uma ferramenta faz pequenas mudanças no seu código, como trocar > por >=, + por - ou retornar um valor vazio, e roda seus testes contra cada mudança. Se os testes continuam passando, o mutante “sobreviveu”, e você achou um ponto onde os testes não verificam de verdade o comportamento.

Ferramentas comuns:

  • JavaScript e TypeScript: StrykerJS. Configure com npm init stryker@latest e rode npx stryker run.
  • Python: mutmut. Rode mutmut run e depois mutmut browse para percorrer os mutantes sobreviventes. Versões recentes precisam de suporte a fork, então no Windows ele roda sob WSL.
  • Java: PIT, por exemplo mvn test-compile org.pitest:pitest-maven:mutationCoverage.
  • Rust: cargo-mutants, instalado com cargo install cargo-mutants e executado com cargo mutants.

Execuções de mutação são lentas numa base de código grande, então limite-as ao módulo em que você está trabalhando. Depois entregue os sobreviventes ao agente:

Estes mutantes sobreviveram em discount.ts: (lista). Para cada um, escreva um teste que falhe no mutante e passe no original. Não mude discount.ts.

Esse é um dos melhores usos de um agente em testes. A tarefa é precisa, a verificação de sucesso é mecânica e o resultado é um teste que comprovadamente pega um tipo real de bug.

Um agente escreve os testes, outro escreve o código

O jeito mais confiável de impedir um agente de entortar os testes é garantir que o agente que escreve o código não possa tocá-los.

A configuração é simples:

  1. Agente de testes. Recebe a especificação e escreve os testes em tests/ (ou onde seus testes ficam). Ele não vê nem escreve a implementação. Os testes dele devem falhar no início, porque a funcionalidade ainda não existe.
  2. Agente implementador. Recebe a mesma especificação mais os testes que falham e escreve o código até eles passarem. Não pode editar nada em tests/.
  3. Divergências voltam. Se o implementador acha que um teste está errado, ele pergunta ao agente de testes, ou a você, em vez de editá-lo.

Isso é TDD dividido entre dois agentes, e funciona entre fornecedores diferentes também. Algumas equipes preferem um modelo escrevendo os testes e outro implementando, para que os dois não compartilhem os mesmos pontos cegos.

Rodar dois agentes na mesma funcionalidade exige algum isolamento para que um não atrapalhe o outro. Um git worktree separado por agente é a resposta usual; veja git worktrees para agentes em paralelo. Para essa divisão específica, você também pode manter os dois no mesmo worktree, já que eles escrevem em pastas diferentes.

Revisando testes escritos por IA

Leia os testes com pelo menos o mesmo cuidado que o código. Um checklist curto:

  • Os valores esperados vêm das regras? Procure literais, não fórmulas copiadas do código.
  • Este teste falharia se o código estivesse errado? Escolha um teste e quebre o código à mão. Se continuar verde, o teste é decoração.
  • Algo importante está mockado? Mocks na fronteira são normais; mocks do código sob teste não são.
  • Algum teste existente mudou? Numa branch de funcionalidade, valores esperados alterados em testes antigos merecem uma pergunta. Num diff, git diff --stat -- tests/ mostra isso num relance.
  • Os nomes são legíveis? O nome de um teste que falha deve dizer o que quebrou.
  • É instável? Fique de olho em tempo real, dados aleatórios, chamadas de rede e ordem dos testes. Peça ao agente para rodar os testes novos várias vezes.

Code review automático também ajuda aqui. Um segundo modelo lendo o diff costuma notar um teste que não afirma nada. O post code review com IA explica como isso funciona.

Erros comuns

  • “Escreva testes para este arquivo” e mais nada. Você recebe a implementação, reescrita.
  • Perseguir um número de cobertura. Você recebe linhas executadas, não comportamento verificado.
  • Deixar quem implementa editar os testes. O caminho mais rápido para o verde é uma asserção alterada.
  • Atualizar snapshots sem ler. -u transforma uma regressão no novo resultado esperado.
  • Escrever testes para código que você está prestes a apagar. Teste o comportamento que vai ficar.
  • Manter testes instáveis. Um agente que vê um teste instável falhar vai “corrigir” de bom grado algo que não estava quebrado. O mesmo vale para corrigir falhas de CI com IA.

Fazendo isso no AgentVera

O AgentVera roda CLIs de agentes de código lado a lado por projeto, então montar o agente de testes e o implementador leva um ou dois minutos.

  • Dois agentes, uma funcionalidade. Abra um agente de testes e um implementador no mesmo projeto, na mesma CLI ou em CLIs diferentes, como Claude Code para os testes e Codex para o código. Coloque-os num worktree separado pelo painel de worktrees se quiser o trabalho fora da sua pasta principal.
  • A posse de arquivos mantém o implementador fora dos testes. No diálogo do agente de testes, adicione tests/** às áreas que ele possui. Enquanto ele roda, outros agentes não conseguem editar esses arquivos. Para agentes Claude Code, a edição é recusada antes de acontecer por um hook PreToolUse; o Codex e outras CLIs recebem a regra nas instruções. Tentativas recusadas aparecem na linha do tempo da equipe.
  • Mensagens entre agentes para divergências. Quando o implementador acha que um teste está errado, ele pode mandar mensagem direto para o agente de testes e esperar a resposta, em vez de editar o teste.
  • O Verify confere o resultado. Escreva na sua mensagem o que “pronto” significa, por exemplo “todos os testes em tests/billing passam e nenhum arquivo de teste mudou”. O Verify transforma isso em critérios e, quando o turno do agente termina, um verificador separado roda os testes e marca cada critério com evidências. O verificador pode ler o repositório e rodar comandos de teste, mas não pode editar arquivos.
  • O code review lê os testes. O code review automático verifica as mudanças de cada turno com o modelo Claude que você escolher e lista os achados com severidade e file:line, o que é uma boa segunda olhada em asserções fracas.
  • Checkpoints para turnos ruins. Cada turno vira um snapshot, então se um agente reescrever metade da suíte de testes numa direção que você não gosta, você desfaz aquele turno e tenta de novo. Veja como desfazer mudanças do Claude Code.

Concluindo

A IA deixa escrever testes barato, o que significa que o trabalho passa de digitar testes para julgá-los. Dê ao agente comportamento em vez de código, mantenha os valores esperados fora do alcance da implementação, use a cobertura como mapa e o mutation testing como nota, e não deixe o agente que escreve o código decidir também o que significa “passar”. Faça isso e a velocidade é real: uma suíte de testes que cresce rápido e ainda falha quando algo quebra.

Leia mais:

Para experimentar o AgentVera, comece pela página inicial.

Perguntas frequentes

A IA consegue escrever bons testes unitários?

Sim, se você conduzir. Agentes são bons em setup, fixtures, listas de casos-limite e boilerplate. São fracos quando tiram os valores esperados do próprio código testado, porque aí o teste só repete a implementação. Dê a eles o comportamento esperado e confira os testes com mutation testing.

O que é um teste tautológico?

Um teste que passa por construção: calcula o valor esperado do mesmo jeito que o código, faz mock justamente do que deveria verificar ou só afirma que uma função foi chamada. Ele continua verde mesmo quando a lógica está errada.

100% de cobertura é uma boa meta para testes escritos por IA?

Não. A cobertura diz quais linhas rodaram, não se algo foi verificado. Use-a para encontrar áreas sem teste e julgue os testes com mutation testing ou uma revisão cuidadosa.

O mesmo agente deve escrever os testes e o código?

Para mudanças pequenas, tudo bem. Para lógica importante, dividir ajuda: um agente escreve testes a partir da especificação, outro implementa, e quem implementa não pode mudar os testes. Isso barra a trapaça mais fácil, editar o teste até ele passar.

Quais agentes posso usar para isso no AgentVera?

Qualquer agente de CLI que o AgentVera roda, como Claude Code, Codex ou Gemini CLI. A posse de arquivos bloqueia edições de antemão para agentes Claude Code; o Codex e outras CLIs recebem as regras de posse nas instruções.

Reúna seus agentes em uma só mesa.

Baixe o AgentVera de graça; as CLIs que você já instalou estão prontas para usar.

Mais artigos