Pular para o conteúdo

Testes com IA

Ângulo 1 — Usar LLMs para gerar e assistir testes

Seção intitulada “Ângulo 1 — Usar LLMs para gerar e assistir testes”
  • Boilerplate e setup: montar @BeforeEach, fixtures, dados de exemplo, conversões de DTO — trabalho mecânico onde o LLM economiza tempo real.
  • Enumerar casos de borda: pedir “liste os casos de borda desta função” costuma render entradas que você não pensou (nulo, vazio, limites, overflow, negativos).
  • Tabelas de @ParameterizedTest: gerar @CsvSource a partir de uma especificação textual.
  • Traduzir intenção em asserts AssertJ fluentes quando você já sabe o que verificar.
  • Explicar um teste legado que ninguém entende antes de refatorá-lo.
  • Dê a especificação, não o código: se você cola a implementação e pede “escreva testes”, o LLM tende a espelhar o código — inclusive seus bugs. Descreva o comportamento esperado e deixe o teste ser independente da implementação.
  • Exija a stack e o estilo: “JUnit 5 + AssertJ, sem Mockito onde não for necessário, um comportamento por teste, nomes descritivos em português”.
  • Peça o teste falhando primeiro (mentalidade TDD): “escreva o teste para este requisito que ainda não implementei”.
  • Force os casos negativos: LLMs enviesam para o happy path; peça explicitamente exceções, limites e entradas inválidas.
  • Itere em passos pequenos: um comportamento por vez, revisando cada bloco.
  • Testes tautológicos: asserts que sempre passam (isNotNull, ou reafirmar exatamente o que o mock foi mandado retornar).
  • Falsa cobertura: exercita linhas sem verificar nada relevante — cobertura sobe, confiança não (ver Fundamentos da Automação de Testes).
  • Asserts fracos: assertThat(lista).isNotEmpty() onde o correto seria containsExactly(...).
  • Supermocking: mocka objetos de domínio que deveriam ser reais, testando a fiação (ver Test Doubles - Stub, Spy, Mock e Fake).
  • Espelhar o bug: ao gerar teste a partir do código, o LLM assume que o código está certo e cristaliza o defeito como “esperado”.

Exemplo típico de teste tautológico gerado por IA — passa mesmo com a lógica errada:

@Test
void calculaImposto() {
var calc = new CalculadoraImposto();
when(tabela.aliquota(any())).thenReturn(new BigDecimal("0.10"));
BigDecimal r = calc.calcular(new BigDecimal("100"), tabela);
assertThat(r).isNotNull();
}

Correção — assert que fixa o valor esperado, derivado da regra e não da implementação:

@Test
void aplicaAliquotaDeDezPorCentoSobreABase() {
var calc = new CalculadoraImposto();
when(tabela.aliquota(any())).thenReturn(new BigDecimal("0.10"));
BigDecimal r = calc.calcular(new BigDecimal("100"), tabela);
assertThat(r).isEqualByComparingTo("10.00");
}

Ângulo 2 — Revisar criticamente testes escritos por IA

Seção intitulada “Ângulo 2 — Revisar criticamente testes escritos por IA”

Trate código de teste gerado como PR de um júnior apressado: útil, mas culpado até prova em contrário. Checklist de revisão:

  • O assert verifica comportamento real? Nada de isNotNull solitário; o valor esperado está literal e correto?
  • O teste falharia se a regra quebrasse? Faça o teste mental: altere a implementação de propósito — o teste pega? (Formalize isso com mutation testing, abaixo.)
  • O esperado foi derivado da especificação ou copiado do código? Se copiado, ele apenas congela o comportamento atual, bug incluso.
  • Há mock demais? Objetos de valor/domínio deveriam ser reais.
  • Só verifica interação onde estado bastaria? Prefira verificação de estado (mais resistente a refactoring).
  • Um comportamento por teste? Ou o LLM empilhou cinco asserts sem relação?
  • Nome descreve a regra? test1/testCalcular não passam.
  • É determinístico? Sem dependência de tempo, ordem, locale (Princípio FIRST).
  • Cobre os negativos? Exceções, nulos, limites — não só o happy path.
  • Está na camada certa? O LLM às vezes sobe @SpringBootTest para o que seria unitário puro (Tipos de Teste - Unit, Integration e E2E).

Duas técnicas medem e ampliam o poder da suíte de forma objetiva, independente de quem escreveu os testes.

Mutation testing responde à pergunta que a cobertura não responde: “meus testes de fato pegam bugs?”. A ferramenta introduz pequenas mutações no código de produção (troca + por -, > por >=, remove uma chamada, nega uma condição) e roda a suíte. Se algum teste falha, o mutante foi “morto” — bom. Se a suíte continua verde, o mutante sobreviveu — há um buraco: aquela linha está coberta mas não verificada.

O mutation score (mutantes mortos / total) é uma métrica de qualidade muito mais honesta que a cobertura de linha, e expõe exatamente os testes tautológicos que uma IA (ou um humano) tende a produzir.

<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.16.1</version>
<configuration>
<targetClasses>
<param>com.exemplo.dominio.*</param>
</targetClasses>
<targetTests>
<param>com.exemplo.dominio.*Test</param>
</targetTests>
</configuration>
<dependencies>
<dependency>
<groupId>org.pitest</groupId>
<artifactId>pitest-junit5-plugin</artifactId>
<version>1.2.1</version>
</dependency>
</dependencies>
</plugin>
Terminal window
mvn org.pitest:pitest-maven:mutationCoverage

O teste tautológico do imposto (com isNotNull) daria cobertura de linha alta e mutation score baixíssimo: qualquer mutação na fórmula do cálculo (trocar multiply por add, negar uma condição, remover uma chamada) passaria despercebida, porque o assert nunca olha o valor. PIT delataria imediatamente.

Em vez de exemplos fixos, você declara propriedades invariantes que devem valer para toda entrada, e o framework gera centenas de casos aleatórios (incluindo bordas que ninguém lembra) tentando falsificar a propriedade. Ao achar um contraexemplo, faz shrinking — reduz ao menor caso que ainda quebra.

class OrdenacaoProperties {
@Property
void ordenarNaoAlteraTamanho(@ForAll List<@IntRange(min = -1000, max = 1000) Integer> lista) {
List<Integer> ordenada = Ordenador.crescente(lista);
assertThat(ordenada).hasSameSizeAs(lista);
}
@Property
void resultadoFicaEmOrdemNaoDecrescente(@ForAll List<Integer> lista) {
List<Integer> ordenada = Ordenador.crescente(lista);
assertThat(ordenada).isSorted();
}
@Property
void ordenarEIdempotente(@ForAll List<Integer> lista) {
List<Integer> uma = Ordenador.crescente(lista);
List<Integer> duas = Ordenador.crescente(uma);
assertThat(duas).isEqualTo(uma);
}
}

Propriedades boas são as invariantes do domínio: round-trip (decode(encode(x)) == x), comutatividade, idempotência, preservação de tamanho/soma. Elas cobrem um espaço de entradas que exemplos manuais — e listas geradas por IA — jamais alcançam.

graph LR
    IA["IA gera rascunho de testes"] --> REV["Humano revisa criticamente"]
    REV --> PIT["PIT mede se os testes pegam bugs"]
    PIT --> PBT["jqwik amplia o espaço de entradas"]
    PIT -.mutantes vivos.-> REV

A IA acelera a escrita; a revisão humana filtra o ruído; o mutation testing mede objetivamente a qualidade e expõe o que a IA maquiou; o property-based amplia a cobertura de espaço de entrada. Nenhuma substitui as outras.

  • Aceitar teste de IA sem revisar: é a via expressa para falsa cobertura em escala.
  • Gerar testes a partir do código de produção: cristaliza bugs como comportamento esperado; parta da especificação.
  • Usar cobertura de linha para “provar” que os testes de IA são bons: cobertura não mede força de assert; use mutation score.
  • Confiar no happy path que o LLM prioriza: force casos negativos e limites explicitamente.
  • Property-based com propriedade fraca (assertThat(r).isNotNull() para toda entrada): mesma tautologia, agora com 1000 execuções.
  • Deixar mutation testing rodar na suíte inteira sempre: é caro; foque no núcleo de domínio e rode em pipeline dedicado.

1. Por que gerar testes a partir do código de produção é arriscado?

Resposta

Porque o LLM assume que o código está correto e escreve asserts que espelham o comportamento atual — bugs incluídos — como se fossem o esperado. O teste então apenas congela a implementação e nunca detectaria o defeito. Deve-se partir da especificação de comportamento, não do código.

2. O que o mutation testing mede que a cobertura de linha não mede?

Resposta

A capacidade real dos testes de detectar bugs. Ele muta o código e verifica se algum teste falha; mutantes sobreviventes indicam linhas cobertas mas não verificadas. O mutation score expõe testes tautológicos que a cobertura de linha aprovaria.

3. Cite três itens do checklist para revisar um teste escrito por IA.

Resposta

Por exemplo: (1) o assert verifica um valor concreto derivado da especificação, não isNotNull; (2) o teste falharia se a regra fosse quebrada de propósito; (3) não há supermocking de objetos de domínio. Outros válidos: um comportamento por teste, determinismo, cobertura de casos negativos, camada correta.

4. O que caracteriza uma boa propriedade em property-based testing?

Resposta

Uma invariante que deve valer para toda entrada do domínio — round-trip (decode(encode(x)) == x), idempotência, comutatividade, preservação de tamanho ou soma. Propriedades fracas como “resultado não nulo” são tautológicas mesmo executadas mil vezes; a força vem de afirmar uma relação real que um contraexemplo poderia falsificar.