Pular para o conteúdo

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.

dataforge
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.

dataforge
r := Crucible.mutar("src/calculo.df",
                    lambda => Crucible.run()["falhas"] bigger 0)

out Crucible.relatorio_de_mutacao(r)
expect(r["sobreviventes"]).to_be_empty()
text
  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 * desconto

A primeira sobrevivente diz que nenhum teste passa exatamente 18 — o caso de borda. A segunda, que ninguém confere o valor com desconto.

DecisãoPor quê
uma troca por mutantecom duas, um teste que pega a primeira esconde a segunda
fronteira de palavrabigger é pedaço de bigger_eq; sem ela o relatório descreveria uma troca e faria outra
comentário e texto não mutammudar um literal ali não muda comportamento, e o mutante sobreviveria sempre
suíte que estoura conta como pegoo código quebrado não passou despercebido, que é a única pergunta
o arquivo volta num finallyum 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.

TrocaO que ela simula
bigger_eqbiggerafrouxa um limite — o caso de borda
smaller_eqsmallero mesmo, do outro lado
biggersmallerinverte a comparação
is notisinverte a igualdade
andortroca o conectivo
+- · */troca a operação
yesnoinverte um literal lógico

Onde continuar#