TDD na era da IA: quando teste verde deixa de significar código correto

TDD na era da IA: quando teste verde deixa de significar código correto

Quem já pediu para a IA "escrever os testes desse serviço" conhece a cena: em poucos segundos aparecem vinte, trinta casos, a cobertura sobe e o pipeline fica verde. Dá uma sensação ótima de dever cumprido. O problema é que, olhando com calma, muitos desses testes não pegariam bug nenhum. Alguns até garantem que o bug continue lá. E todos eles rodam a cada pull request, deixando o CI mais lento. Neste artigo eu junto o que estudos e times reais estão mostrando sobre isso, mas também o lado bom: como usar a própria IA para escrever testes melhores e para revisar a suíte que você já tem.

O teste que passa e não protege nada

Um teste só vale alguma coisa se ele quebra quando o comportamento quebra. Pense numa regra de negócio simples: "cliente VIP tem 10% de desconto". Este é o tipo de teste que aparece muito quando a gente pede "cubra essa classe com testes":

public function test_calculate_returns_value(): void
{
    $service = new DiscountService();

    $result = $service->calculate(100.0, 'VIP');

    $this->assertNotNull($result);
}

A linha foi executada, então a ferramenta de cobertura conta como coberta. Só que qualquer número faz esse teste passar. Se alguém trocar o desconto para 50% sem querer, ele continua verde.

Tem uma versão mais disfarçada, que parece teste de verdade:

public function test_calculate_applies_vip_rate(): void
{
    $service = new DiscountService();

    $expected = 100.0 * (1 - DiscountService::VIP_RATE);

    $this->assertEquals($expected, $service->calculate(100.0, 'VIP'));
}

Aqui o teste refaz a conta usando a constante da própria classe. Se a constante estiver errada, o teste calcula o mesmo valor errado e aprova. Ele virou um espelho do código. O teste que protege a regra é o que escreve o resultado esperado com todas as letras:

public function test_vip_customer_gets_ten_percent_off(): void
{
    $service = new DiscountService();

    $this->assertSame(90.0, $service->calculate(100.0, 'VIP'));
}

A diferença entre os três está no que o pessoal de testes chama de oráculo, que é a fonte da verdade usada para dizer se o resultado está certo. No terceiro, o oráculo é a regra de negócio. Nos outros dois, é o próprio código. Essa ideia volta várias vezes no texto.

O que os estudos andam mostrando

Um dos primeiros trabalhos grandes sobre o assunto foi o estudo de Zhiqiang Yuan e colegas, apresentado no FSE 2024. Eles avaliaram testes unitários gerados pelo ChatGPT e acharam muito teste que nem compilava ou que falhava por asserção errada. Os que passavam, por outro lado, ficavam parecidos com os escritos por gente em cobertura e legibilidade. Ou seja, olhando por cima, pareciam bons.

Aqui no Brasil, um TCC da UnB de Lucas Gabriel Bezerra, orientado pela professora Elaine Venson, fez um teste parecido em um projeto real: 200 classes do ZAP Proxy, com o GPT-3.5 Turbo gerando testes. A quantidade de testes cresceu 5,7% (de 2.784 para 2.944), mas a cobertura subiu só 0,2%. Mais de 60% das suítes geradas ainda tinham erro de compilação depois de três tentativas de correção. Mais teste, quase nenhuma proteção a mais.

Quando a régua deixa de ser cobertura, a coisa fica mais clara. Uma revisão de literatura de 2025 reúne resultados de teste de mutação, uma técnica que insere pequenos defeitos de propósito no código (trocar um > por >=, por exemplo) e vê se algum teste percebe. Em um dos estudos citados, os testes gerados por LLM mataram no máximo 54,6% dos mutantes, contra 69% dos testes escritos por pessoas. Em outro, o número ficou perto de zero, porque os testes se concentravam em interfaces e métodos vazios.

A mesma revisão aponta o efeito mais traiçoeiro. Quando o código já tem um bug, o assistente tende a gerar testes que confirmam o bug, porque trata o código recebido como a verdade. É o caso do teste espelho que vimos lá em cima, só que em escala.

O Emerson Bezerra resumiu bem na DIO: a IA não enxerga contexto de negócio, regras implícitas e prioridade de risco, e "testes gerados automaticamente podem passar sem validar o que realmente importa".

Tem ainda o volume. Num caso relatado pelo DevOps.com, o modelo gerou 26 testes para um endpoint onde um QA sênior escreveria 9, vários deles sendo o mesmo caso com nome diferente. E um estudo de 2024 sobre test smells (padrões que deixam a suíte difícil de manter) mostrou que testes longos e testes inúteis costumam andar juntos nas suítes geradas.

Quando o agente apaga o teste

Com os agentes de código, que editam arquivos e rodam comandos sozinhos, apareceu um problema novo. Kent Beck, o criador do TDD, contou no podcast The Pragmatic Engineer que vive brigando para os agentes não apagarem testes só para a suíte "passar".

Ele compara o agente a um gênio da lâmpada meio imprevisível: realiza o desejo, mas do jeito dele. Se o pedido é "deixe os testes verdes", apagar o teste que falha cumpre o pedido. No texto Augmented Coding: Beyond the Vibes, Beck coloca como sinal de alerta qualquer indício de trapaça, como desativar ou apagar teste.

A ironia é que a gente lê um teste falhando como um requisito. O agente pode ler como um obstáculo no caminho. Por isso, diff que mexe em teste merece mais atenção na revisão do que diff que mexe em código.

O CI pagando a conta

Todo teste custa tempo de máquina em cada pull request. Quando a IA multiplica a quantidade de testes, o CI (o pipeline de integração contínua, que roda build e testes a cada mudança) vira gargalo.

O exemplo mais recente vem da Linear. Em um post de setembro de 2026, a empresa contou que agentes já escrevem a maior parte dos seus testes. O repositório ganha uns 2.000 testes por semana e a suíte quase quadruplicou desde janeiro. Se nada fosse feito, cada PR ia esperar cerca de 11 minutos. Depois de reorganizar o pipeline, a espera ficou em pouco mais de 5 minutos. A RuntimeWire detalhou as mudanças.

O retrato mais amplo vem do relatório DORA 2025, pesquisa do Google Cloud com quase 5.000 profissionais. A adoção de IA passou a andar junto com mais entregas, mas também com mais instabilidade: mais deploy que dá problema, mais retrabalho, mais tempo para resolver. A leitura dos autores é que a IA amplifica o que o time já tem. Time com boa rede de testes acelera. Time com testes frágeis entrega mais rápido o que quebra.

No dia a dia, pipeline lento quebra o foco, porque você sai da tarefa enquanto espera. E teste instável, aquele que falha sem ninguém ter mexido no código, ensina o time a clicar em "rodar de novo" sem olhar. Depois de algumas semanas assim, o vermelho no CI deixa de significar alguma coisa.

É por isso que começaram a aparecer relatos de limpeza. Num texto na DEV Community, um time conta que removeu ou reescreveu testes redundantes, instáveis e de pouco valor e derrubou o CI de 40 para 16 minutos. Em outro relato, uma equipe viu a cobertura subir com testes gerados, o CI ficar verde e mesmo assim um bug chegar em produção, porque as asserções só checavam se "a resposta existe".

A IA também pode ser parte da solução

Até aqui parece que a conclusão é "não use IA para testes". Não é. O que os casos que deram certo têm em comum é que a IA recebe um objetivo melhor do que "gere testes para esta classe".

A Meta é o exemplo mais forte. O sistema ACH usa um LLM para criar mutantes, defeitos simulados que nenhum teste atual pega, e depois pede ao LLM um teste que pegue exatamente aquele defeito. Rodou em mais de 10 mil classes Kotlin de sete plataformas, gerou 571 testes, e os engenheiros aceitaram 73% deles nas revisões. Cada teste tem um alvo: um defeito que nenhum teste pegava.

A própria Linear, que sofreu com o volume, resolveu parte do problema mexendo nas instruções dos agentes, para que os testes gerados já sigam as regras de desempenho do projeto. O agente continuou escrevendo testes, só que com as regras da casa.

E teve pesquisa brasileira testando TDD com IA de forma estruturada. Na monografia de Ivan Vilaça de Assis na UFMG, orientada pelo professor Marco Túlio Valente, o fluxo AI-TDD divide o trabalho em três papéis de prompt: arquiteto, escritor de testes e implementador. O ciclo de falhar primeiro e passar depois funcionou em 100% das tentativas, e as regras de negócio ficaram mais bem generalizadas. O resultado não foi só vitória: a abordagem de código primeiro teve escore de mutação e cobertura melhores. O autor atribui essas diferenças a pontos específicos e corrigíveis do processo, não a um defeito da ideia de testar antes. Para o dia a dia, fica uma lição: separar quem define o teste de quem escreve o código ajuda, mas não dispensa medir.

Como fazer a IA escrever testes melhores

Agora a parte prática. Os exemplos estão em PHP com Pest e PHPUnit, mas a lógica vale para qualquer linguagem.

Dê a regra, não só o código

Se a única coisa que o modelo recebe é a classe, a única verdade que ele conhece é a classe. Mande junto a regra de negócio em linguagem de gente. Um prompt que funciona bem é algo assim:

Escreva testes Pest para DiscountService.

Regras de negócio (esta é a fonte da verdade, não o código):
- Cliente REGULAR não tem desconto.
- Cliente VIP tem 10% de desconto.
- Pedido acima de R$ 500,00 tem frete grátis. Exatamente R$ 500,00 não tem.
- Valor negativo lança InvalidArgumentException.

Antes de escrever código, me mostre uma tabela com:
entrada | resultado esperado | qual regra o caso protege.

Regras para os testes:
- O valor esperado deve ser um literal (ex.: 90.0), nunca calculado
  com constantes ou métodos da classe testada.
- Nada de assertNotNull, toBeTruthy ou assertTrue isolados.
- Se o código atual contrariar alguma regra, NÃO ajuste o teste:
  me avise da divergência.

Repare em três detalhes. A tabela antes do código deixa você revisar os casos em 30 segundos, antes de existir uma linha de teste. A proibição de calcular o esperado mata o teste espelho. E a última instrução transforma o modelo em detector de bug, em vez de carimbador do bug.

Peça bordas e datasets, não variações de nome

Em vez de dez testes quase iguais, peça um teste com dataset cobrindo as bordas. Fica mais curto, mais rápido de rodar e mais fácil de ler:

covers(DiscountService::class);

it('applies the discount by customer tier', function (string $tier, float $expected) {
    $service = new DiscountService();

    expect($service->calculate(100.0, $tier))->toBe($expected);
})->with([
    'regular customer' => ['REGULAR', 100.0],
    'vip customer' => ['VIP', 90.0],
]);

it('gives free shipping only above 500', function (float $total, bool $expected) {
    $service = new ShippingService();

    expect($service->isFree($total))->toBe($expected);
})->with([
    'just below' => [499.99, false],
    'exactly 500' => [500.00, false],
    'just above' => [500.01, true],
]);

it('rejects negative values', function () {
    (new DiscountService())->calculate(-1.0, 'VIP');
})->throws(InvalidArgumentException::class);

Uma frase no prompt já resolve: "agrupe casos da mesma regra em um dataset e inclua o valor exatamente na fronteira, um abaixo e um acima".

Mostre um teste bom da casa

O modelo copia o padrão que vê. Se o projeto tem factories, helpers e um jeito próprio de montar cenário, cole um teste bem escrito do próprio repositório no prompt e diga "siga este modelo". Isso evita o clássico teste que monta usuário na mão com quinze campos quando já existe User::factory().

Use os mutantes como roteiro

De todas as dicas, é a que eu colocaria em prática primeiro. É a ideia da Meta em versão caseira. O Pest tem teste de mutação embutido. Basta marcar o que o teste cobre com covers() e rodar:

./vendor/bin/pest --mutate --parallel

Ele altera pedacinhos do código e roda os testes de novo. Toda mutação que passa sem nenhum teste falhar aparece como "untested", com o diff. Algo como:

app/Services/ShippingService.php
-  return $total > 500;
+  return $total >= 500;

Isso quer dizer que nenhum teste checa o valor exato de R$ 500. Agora você leva o mutante para a IA:

Este mutante sobreviveu à suíte:
[cole o diff]

Escreva UM teste que falhe com a mutação e passe com o código
original. Use o valor de fronteira. Não altere nenhum teste existente
nem o código de produção.

O pedido é pequeno e dá para conferir: você roda a mutação de novo e vê se ela morreu. Para quem usa PHPUnit, o Infection faz o mesmo papel, e dá para limitar a mutação aos arquivos alterados no diff para não pesar no CI. Em JavaScript e TypeScript existe o Stryker, e em Python o mutmut. O Vinícius Dias tem uma palestra ótima explicando teste de mutação com Infection, e o argumento dele continua atual: 100% de cobertura não garante que o teste pega falha.

Separe quem escreve o teste de quem escreve o código

Quando a mesma sessão escreve o código e o teste, o teste tende a confirmar o código. Uma saída simples, na linha do AI-TDD da UFMG, é dividir: primeiro uma conversa só para os testes, a partir da regra, que você revisa. Depois outra conversa para a implementação, com a instrução de que os testes não podem ser alterados. Se o agente achar que um teste está errado, ele tem que parar e perguntar.

Deixe as regras escritas para o agente

Ferramentas como Claude Code, Cursor e Codex leem arquivos de instrução na raiz do projeto, como CLAUDE.md ou AGENTS.md. Vale colocar ali uma seção de testes, algo assim:

## Testes
- Nunca apague, pule (skip) ou comente um teste para fazer a suíte passar.
  Se um teste parece errado, pare e pergunte.
- Valor esperado sempre literal, vindo da regra de negócio.
- Use as factories de database/factories e os helpers de tests/Support.
- Casos da mesma regra vão em dataset, com valores de fronteira.
- Todo teste novo precisa de covers() e deve matar pelo menos
  um mutante em ./vendor/bin/pest --mutate.
- Teste unitário não acessa banco nem rede.

E, por segurança, uma checagem no CI que avise quando um PR remove testes já ajuda muito. Um git diff --stat na pasta tests/ mostrando linhas removidas é suficiente para chamar atenção na revisão.

Como revisar a suíte que você já tem

Se a sua base já tem muitos testes gerados por IA, dá para fazer uma auditoria sem jogar tudo fora. A ideia é medir antes, limpar com critério e medir depois.

1. Meça o ponto de partida

Anote três números: tempo total da suíte no CI, escore de mutação dos módulos mais importantes e quantos testes falharam de forma intermitente no último mês. O Pest mostra os testes mais lentos com ./vendor/bin/pest --profile. Para o escore, rode a mutação só nas classes críticas, por exemplo ./vendor/bin/pest --mutate --class="App\Services", porque rodar no projeto inteiro pode demorar bastante.

2. Cace asserções fracas

Um grep rápido já mostra onde olhar primeiro:

grep -rnE "assertNotNull|assertTrue\(true\)|toBeTruthy|toBeInstanceOf|markTestSkipped|->skip\(" tests/

Nem todo resultado é problema (às vezes checar o tipo faz sentido), mas é uma boa lista de suspeitos. Teste sem nenhuma asserção também entra: o PHPUnit avisa sobre eles como "risky", então vale ler esse aviso em vez de ignorar.

3. Use a IA como revisora, não como faxineira

A IA é muito boa para ler um arquivo de teste inteiro e classificar o que tem ali. O cuidado é não deixar ela apagar nada sozinha. Um prompt de auditoria que funciona:

Revise o arquivo de testes abaixo junto com a classe que ele testa.
Para cada teste, preencha uma tabela:

teste | o que protege (regra ou comportamento) | problema | ação

Ações possíveis:
- MANTER: protege regra, borda, integração ou bug já corrigido.
- REFORÇAR: protege algo importante, mas a asserção é fraca.
  Mostre a asserção nova.
- FUNDIR: repete outro caso. Diga com qual e sugira um dataset.
- REMOVER: não falharia se o comportamento quebrasse.
  Explique qual mudança no código passaria despercebida.

Não edite nenhum arquivo. Na dúvida, classifique como MANTER
e explique a dúvida.

A coluna "o que protege" é o pulo do gato. Se nem você nem a IA conseguem dizer qual regra um teste protege, ele é forte candidato a sair. E pedir para a IA explicar qual mudança passaria despercebida obriga a justificativa a ser concreta.

4. Limpe em PR separado e prove com números

Faça a limpeza num PR só de testes, sem mexer em código de produção. Antes de aprovar, rode a mutação de novo. Se o escore não caiu e o tempo de CI diminuiu, a limpeza está provada. Se o escore caiu, algum teste removido protegia algo, e ele volta.

Para ajudar na decisão, esta tabela resume os sinais mais comuns:

Sinal no teste O que costuma significar O que fazer
Só checa se o retorno existe ou não é nulo Não pega mudança de valor Reforçar com valor literal
Calcula o esperado com a constante ou método da classe Teste espelho, aprova bug Trocar pelo valor da regra de negócio
Vários testes iguais com nomes diferentes Volume sem cobertura nova Fundir em um dataset
Mock para tudo, inclusive o que está sendo testado Testa o mock, não o código Reescrever ou remover
Falha às vezes sem mudança no código Teste instável, corrói a confiança no CI Corrigir ou isolar, nunca só rodar de novo
Cobre um bug que já foi corrigido Proteção contra regressão Manter, mesmo que pareça simples

5. Transforme a auditoria em rotina

Depois da primeira rodada, coloque um escore mínimo de mutação no CI só para os arquivos alterados no PR (no Pest, a opção --min faz a execução falhar abaixo do valor definido). Assim todo teste novo, escrito por pessoa ou por IA, precisa provar que pega pelo menos algum defeito. É o critério que a cobertura de linhas nunca conseguiu dar.

Fechando

A IA deixou a escrita de testes muito barata. Ler, manter e rodar esses testes continua custando a mesma coisa, e quem paga é o time inteiro, a cada pull request. Só que a mesma IA que gera teste inútil escreve teste bom quando recebe a regra de negócio e um mutante para matar, e quando alguém confere o resultado. A pergunta que vale fazer para a sua suíte deixou de ser "quantos testes temos?" e passou a ser "quais deles falhariam se o sistema quebrasse?".

Referências

Comentários (0)

Nenhum comentário ainda. Seja o primeiro.

Continue lendo

Como acessar o entity Manager(Doctrine) dentro de comandos no Symfony 5

Como acessar o entity Manager(Doctrine) dentro de comandos no Symfony 5

Ao termos de criar comandos personalizados no Symfony na maioria das vezes se faz necessário o uso de alguma interação no banco de dados quer seja a criação, edição ou até mesmo a exclusão de um registro. No exemplo…

Artigo, Doctrine, php, Symfony, Tutorial
Olá! Mundo...(mais um blog de um programador no ar)

Olá! Mundo...(mais um blog de um programador no ar)

Como todo mundo na programação já passou por este clichê, aqui não poderia ser diferente então o primeiro post desse blog será um Hellow world! Espero poder trazer algo de útil a alguém que por um acaso passe por aqui,…

Sem categoria