o problema
Geração de teste por llm tinha, em 2023, um problema de confiança e um problema de escala. O de confiança é a alucinação: o modelo devolve uma classe de teste bonita, e alguém precisa ler linha por linha para descobrir se ela testa alguma coisa. O de escala é que os números publicados vinham de sistemas pequenos. O paper cita o contraste: 80% de cobertura no HumanEval contra no máximo 2% no SF110, e 70% de cobertura em pacotes de 25 a 3.100 linhas. Nada disso diz o que acontece num repositório que recebe mais de 100.000 commits por semana e tem dezenas de milhões de linhas no cliente.
E há o problema do custo de revisão. Um recomendador que manda dez sugestões medíocres para cada boa não é neutro: ele queima a paciência do engenheiro e morre. Para a Meta, a métrica que importava não era cobertura média, era taxa de aceitação em produção. O ponto de comparação interno era o SapFix, o reparo automático deles, com 50% das correções aceitas.
a ideia
Duas inversões. A primeira é de enquadramento: em vez de escrever uma classe de teste do zero, o TestGen-LLM estende uma classe escrita por humano, adicionando casos. Isso resolve não-regressão por construção — os testes antigos continuam lá — e resolve janela de contexto, porque a classe de teste é normalmente muito menor que a classe testada. O paper mostra que dá para achar testes novos passando só a classe de teste, sem a classe sob teste.
A segunda inversão é onde mora a contribuição: o modelo não é a resposta, é o gerador de candidatos. Entre o llm e o revisor humano entra uma cadeia de filtros semânticos que descarta tudo que não puder ser provado melhor. O que sobra vem com documentação automática de quanto melhorou. É genetic improvement clássico — gerar barato, filtrar caro — com o llm no lugar da programação genética. Os autores chamam isso de Assured LLMSE.
como funciona
Quatro prompts, dois modelos internos (chamados só de LLM1 e LLM2, sem detalhes), varredura de temperatura. Os prompts vão do óbvio (extend_coverage: aqui está a classe de teste e a classe testada, escreva uma versão estendida) ao teste de hipótese (statement_to_complete, escrito como frase incompleta para o modelo continuar, já que o modelo é um completador de texto).
Os candidatos passam por três filtros, em ordem crescente de custo:
- build — não compila, morre;
- passes — não passa na primeira execução, morre; e depois cinco execuções seguidas, porque um teste que passa em quatro de cinco é flaky e flakiness em escala industrial é veneno;
- coverage — não aumenta a cobertura de linha sobre todas as classes de teste do mesmo build target, morre.
O filtro de cobertura é medido por caso de teste, não por classe. Isso saiu do primeiro trial: um diff aceito continha quatro testes que eram o mesmo teste com nomes diferentes. Medir por caso é mais lento, mas permite misturar contribuições de modelos, prompts e temperaturas diferentes — o ensemble.
Uma observação virou filtro depois: os testes gerados eram cópias quase literais uns dos outros ou completamente diferentes, nunca “parecidos”. Como a semelhança era tudo-ou-nada, bastou comparar corpos de teste por igualdade sintática para deduplicar.
o que isso custou
O filtro passes descarta qualquer teste que falhe. Um teste que falha pode ter achado um bug — mas sem oráculo automático não dá para saber se é bug ou assertion errada, e o pipeline precisa ser automático. Consequência: a ferramenta produz exclusivamente teste de regressão. Bug novo, não.
Cobertura de linha é o critério de melhoria, e os autores dizem explicitamente que é um proxy conveniente. Cobertura de branch existe no Jacoco mas não roda na escala deles; mutation coverage seria melhor e é cara demais.
O rendimento é baixo. Nos test-a-thons, 1.321 tentativas bem-sucedidas em 32.531 — 0,04 no Instagram, 0,05 no Facebook. Aplicado a 1.979 classes de teste, o tool melhorou 196, cerca de 10%. Dessas melhorias, 73% foram aceitas e landaram. No test-a-thon do app do Facebook, dos 280 diffs, 144 foram aceitos, 64 rejeitados e 61 nem receberam review.
E os filtros não capturam gosto. Os quatro diffs rejeitados no test-a-thon de dezembro caíram por testar um getter, por violar responsabilidade única (dois casos) e por não ter assertion. Nada disso é detectável por build-pass-cover.
Os números de deployment também são confundidos: os testes acumulam cobertura incrementalmente em produção, a cobertura cresce de forma logarítmica, e portanto qualquer configuração que receba mais tentativas parece pior. Por isso a temperatura 0,4 aparecer bem na tabela não significa que o default deva mudar de zero.
onde isso aparece hoje
O padrão gerar-e-filtrar virou o formato default de ferramenta de código com llm: o modelo propõe, um verificador barato e determinístico decide, e o humano só vê o que sobreviveu. O paper é a referência mais citada dessa arquitetura em escala industrial, e existe implementação aberta do desenho — o Cover-Agent, da Qodo, se descreve como uma implementação do TestGen-LLM.
O trabalho também deixou um número comparável no debate sobre produtividade: no primeiro test-a-thon do Instagram, com 36 engenheiros, o TestGen-LLM ficou em sexto lugar por número de testes landados, com 17. A mediana de linhas cobertas por teste dele foi 2,5 — os 1.460 do ranking vêm quase todos de um único caso que cobriu 1.326 linhas de uma vez.