antonio leandro

ia generativa

Automated Unit Test Improvement using Large Language Models at Meta

paper · Nadia Alshahwan, Jubin Chheda, Anastasia Finegenova, et al. ·

a tese

o llm não precisa acertar: ele gera candidatos, e um filtro de compila-passa-cobre joga fora 96% deles — o que chega ao code review é comprovadamente melhor que o teste que já estava lá

o que fica

  1. Estender uma classe de teste existente em vez de escrever do zero garante não-regressão por construção e cabe em janela de contexto pequena, porque a classe de teste costuma ser bem menor que a classe testada.
  2. Um teste gerado que falha é descartado sem investigação: sem oráculo automático não dá para separar bug real de assertion errada, então a ferramenta só produz teste de regressão.
  3. A taxa de acerto por tentativa ficou entre 4% e 5%: das 32.531 tentativas de gerar um caso de teste nos test-a-thons, 1.321 sobreviveram aos filtros.
  4. Nenhum prompt domina: três das quatro estratégias testadas acharam testes que nenhuma outra achou, o que transforma o problema num ensemble em vez de uma caça ao prompt certo.
  5. Os diffs rejeitados não falharam nos filtros — caíram por critério de code review: testar um getter trivial, misturar responsabilidades, esquecer a assertion.
  6. Os llms copiam com fidelidade o estilo do teste vizinho, inclusive utilitários de assertion caseiros, e por isso também reproduzem estilos já deprecados no repositório.

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:

  1. build — não compila, morre;
  2. 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;
  3. 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.

lido na íntegra por pipeline de llm, revisado por antonio leandro antes de publicar ·