Contratos e mutação
O mesmo teste valendo para duas implementações, e a pergunta que a cobertura não responde.
Contrato: um trait, muitas implementações#
Duas implementações do mesmo trait costumam ter dois conjuntos de testes, escritos em épocas diferentes, cobrindo coisas diferentes. A segunda passa nos testes dela e quebra no uso — porque o que ela não cumpre é justamente o que só o teste da primeira cobria.
action guarda_e_le(a):
a.guardar("x", 1)
expect(a.ler("x")).to_be(1)
action apaga(a):
a.guardar("x", 1)
a.apagar("x")
expect(a.ler("x")).to_be_void()
provas := Crucible.contrato("Armazem", [
{"nome": "guarda e le", "prova": guarda_e_le},
{"nome": "apaga", "prova": apaga}
])
provas.para("em memoria", lambda => spawn EmMemoria())
provas.para("em disco", lambda => spawn EmDisco("/tmp/x"))
provas.cobrar()cobrar() levanta nomeando qual implementação falhou em qual caso. A alternativa — um resumo dizendo "2 falhas" — manda procurar em dois lugares.
Mutação: o teste testa mesmo?#
Cobertura responde "esta linha rodou?". Ela não responde "se esta linha estivesse errada, alguém reclamaria?" — e as duas divergem justamente onde importa: um teste que chama a função e não confere o resultado dá 100% de cobertura e zero de proteção.
A mutação responde a segunda pergunta: troca um operador no código, roda a suíte, e vê se ela falha. Se a suíte passar, aquele teste não testava aquilo. O mutante é chamado de sobrevivente, e cada sobrevivente é um buraco com endereço.
r := Crucible.mutar("src/calculo.df",
lambda => Crucible.run()["falhas"] bigger 0)
out Crucible.relatorio_de_mutacao(r)
expect(r["sobreviventes"]).to_be_empty() src/calculo.df
7/9 mutantes pegos (78%)
sobreviveram — ninguém reclamou destas trocas:
linha 12: bigger_eq → bigger (afrouxa um limite: '>=' vira '>')
yield idade bigger_eq 18
linha 27: * → / (troca a operação)
yield valor * descontoA primeira sobrevivente diz que nenhum teste passa exatamente 18 — o caso de borda. A segunda, que ninguém confere o valor com desconto.
| Decisão | Por quê |
|---|---|
| uma troca por mutante | com duas, um teste que pega a primeira esconde a segunda |
| fronteira de palavra | bigger é pedaço de bigger_eq; sem ela o relatório descreveria uma troca e faria outra |
| comentário e texto não mutam | mudar um literal ali não muda comportamento, e o mutante sobreviveria sempre |
| suíte que estoura conta como pego | o código quebrado não passou despercebido, que é a única pergunta |
o arquivo volta num finally | um código-fonte silenciosamente alterado é o pior desfecho de uma ferramenta de teste |
As onze trocas#
Cada uma é uma mudança que um humano faria por engano, e que um teste de verdade pegaria. Trocas que quase sempre produzem erro de sintaxe ou laço infinito ficam de fora: elas gastam uma rodada da suíte para não dizer nada.
| Troca | O que ela simula |
|---|---|
bigger_eq → bigger | afrouxa um limite — o caso de borda |
smaller_eq → smaller | o mesmo, do outro lado |
bigger ↔ smaller | inverte a comparação |
is not → is | inverte a igualdade |
and ↔ or | troca o conectivo |
+ ↔ - · * → / | troca a operação |
yes → no | inverte um literal lógico |