DEV Community

Paulo Gonçalves
Paulo Gonçalves

Posted on • Updated on

Teste de mutação 👽: O que é e como fica a cobertura de código?

Sumário


Esse conteúdo foi apresentado com mais detalhes e profundidade na live do AT Talks em 25/11/20. Para assistir clique aqui.

Who tests the tests?

Quando pensamos em validar a eficiência dos testes implementados, normalmente o que vem à mente é a métrica de cobertura de código. Porém, será que ela realmente é a melhor prática para garantir que os testes estão realmente testando os cenários possíveis?

Cobertura de código é uma métrica que valida o quanto do código foi coberto pelos testes.
Ou seja, verifica quais linhas do código foram executadas ao rodar os testes e retorna o percentual de cobertura.

Leia esse conteúdo (en) e esse (pt-br) para saber mais.

Utilizando apenas a métrica de quantidade de cobertura de código não conseguimos garantir que todos os cenários foram cobertos, apenas... quais linhas foram executadas 😮.

Pense um pouco sobre isso. Alguma vez já viu um teste sem asserção apenas para aumentar a cobertura de código? Tenho certeza que já soube de uma situação parecida.

Claro que nessa situação, para evitarmos engraçadinhos, basta colocarmos alguma biblioteca que valida que todos os testes possuem asserção e que o percentual de cobertura de código está acima de algum número mágico, como 80%.

O problema é que, como dito, a cobertura de código não valida a eficiência dos testes, e vamos ver abaixo o porquê.


Porque cobertura de código não é confiável

Abaixo temos um pequeno método que possui apenas 1 teste validando o seu comportamento.

Disclaimer
É perceptível que esse método possui mais de 1 cenário, porém precisamos de exemplo prático e simples para comparar cobertura de código e teste de mutação.

// ./src/cnh.js
module.exports = podeTirarCarteiraDeMotorista = idade => {
  return idade >= 18
}

// ./__tests__/cnh.spec.js
test('Deve retornar false para pessoa com menos de 18 anos', () => {
  expect(podeTirarCarteiraDeMotorista(17)).toBe(false)
})
Enter fullscreen mode Exit fullscreen mode

Se verificarmos a cobertura de código do arquivo cnh.js, será apresentado que ele foi 100% coberto (aqui o gestor comemora), porém sabemos, por ser um teste simples, que a validação não está eficiente e que poderíamos validar outros cenários, como:

  1. Deve retornar true se idade for igual a 18
  2. Deve retornar true se idade for igual a 19

Cobertura de código de cnh.js:

Print da cobertura de código mostrando score de 100%

E é baseado nessa brecha da métrica de linhas executadas é que o uso do teste de mutação faz sentido.

"...100% code coverage score only means that all lines were exercised at least once, but it says nothing about tests accuracy or use-cases completeness, and that’s why mutation testing matters"
Baeldung, 2018


Testes de mutação

Imagine um sanduíche coberto com uma pasta. Cobertura de código vai te dizer que o pão está 80% coberto com pasta. O teste de mutação, por outro lado, vai dizer que a pasta é chocolate e não... bem... qualquer outra coisa.

O conceito de teste de mutação é bem simples:

Bugs, ou mutantes, são inseridos no código e os testes são executados em cima do código mutado. Se pelo menos 1 dos testes quebrar ou tiver timeout, o mutante é considerado morto 💀 e aquele trecho de código alterado é considerado como coberto pelos testes.

Ainda não está claro? Então vamos lá.

Abaixo está o nosso código original:

// ./src/cnh.js
const podeTirarCarteiraDeMotorista = idade => {
  return idade >= 18
}
Enter fullscreen mode Exit fullscreen mode

O teste de mutação irá detectar todos os pontos que podem ser alterados no código e atuar em cima deles. No nosso exemplo serão feitas as seguintes alterações (serão 5 mutantes no total):

  • A expressão condicional idade >= 18 será alterada para true e false;
  • O operador de idade >= será alterado para < e >;
  • O bloco => { return idade >= 18 } será alterado para => {}.

Quer entender tudo que será mutado no seu código e para o que será mutado? Leia 'Mutantes suportados pelo stryker (en)'.

A cada alteração feita, todos os testes criados são executados. Se algum teste quebrar, significa que aquela alteração (mutação) está coberta, então ela foi assassinada.

É um pouco confuso a questão de que para que aquela mutação seja considerada como morta (sucesso) é preciso que algum teste quebre (afinal, teste quebrar é ruim). Porém temos que entender que o nosso teste foi feito para o cenário ABC e se o cenário foi alterado para ABZ, o nosso teste tem que detectar essa mudança e falhar.

O teste de mutação é nada mais e nada menos do que automatizar todo o processo de "sabotar o código e executar testes para ver se eles falham"

Se executarmos teste de mutação utilizando o teste e código apresentados anteriormente, o resultado seria esse:

Print do teste de mutação mostrando score de 60%, 5 mutações criadas e sendo que 2 delas não foram detectadas pelos testes

Tínhamos 100% de cobertura de código, porém o teste de mutação revelou que 2 mutações criadas não resultaram em quebra do nosso teste (sobreviventes), demonstrando que há brecha no nosso teste.

Para que todos os 5 mutantes não sobrevivam, precisamos criar um novo teste que cubra essa brecha, como:

test('Deve retornar true para pessoa maior de 18 anos', () => {
  expect(podeTirarCarteiraDeMotorista(18)).toBe(true)
})
Enter fullscreen mode Exit fullscreen mode

Detalhe da execução

Quando executamos o teste de mutação são feitas as seguintes etapas:

  1. Analisa quais arquivos serão mutados;
    • No nosso caso foi cnh.js.
  2. Executa todos os testes e espera que todos passem;
    • O teste é abortado se algum teste falhar. Para validar se algum teste quebrou com mutação é imprescindível que todos os testes sejam executados com sucesso com o código original.
  3. Gera mutante para todos os trechos de código;
    • No nosso caso foram 5 mutantes criados.
  4. Executa todos os testes para cada mutante gerado;
  5. A pontuação final do teste é de acordo com a quantidade de mutantes que foram mortos ou resultaram em timeout em comparação com a quantidade total de mutantes.

RIP Cobertura de código?

Embora teste de mutação seja uma métrica muito interessante para entendermos a saúde dos testes criados, é importante salientar que ele NÃO substitui a cobertura de código, atuando apenas como complemento e possui algumas desvantagens que impedem fortemente a sua adoção em larga escala.

Portanto, cobertura de código continuará sendo uma métrica bastante usada e não é uma ferramenta antagonista ao teste de mutação

Desvantagem

Como o teste de mutação analisa todos os possíveis pontos que podem ser mutados no código e executa todos os testes para cada mutação, ele possui uma execução que onera bastante a máquina e possui um alto tempo de execução.

Devido à necessidade de ter um alto poder computacional, o uso do teste de mutação chega a ser proibitivo em projetos médios e grandes.

Um exemplo dessa limitação é o projeto ServeRest. Todos os 86 testes existentes são executados em aproximadamente 550 milissegundos, enquanto os testes de mutação atuam em cima de 22 arquivos, resultando em 599 mutantes e com execução média de 19 minutos.

No nosso código de exemplo a cobertura de código é executada em 9 ms enquanto o teste de mutação é executado em 3 segundos.

Adoção em grandes projetos - Case Google

A seção Adoção em grandes projetos - Case Google será atualizado com os detalhes das estratégias de testes de mutação em breve. Esses detalhes foram apresentados na live do AT Talks.

Essa limitação de poder computacional não impediu a adoção do teste de mutação pela Google nos seus códigos (que possuía 2 bilhões de linhas em 2018), porém ela teve que utilizar de algumas estratégias de criação da mutação.

Traditional mutation analysis is computationally prohibitive which hinders its adoption as an industry standard. In order to alleviate the computational issues, we present a diff-based probabilistic approach to mutation analysis that drastically reduces the number of mutants by omitting lines of code without statement coverage and lines that are determined to be uninteresting - we dub these arid lines.
State of Mutation Testing at Google

No bom português:

A análise de mutação tradicional é computacionalmente proibitiva, o que impede sua adoção como um padrão da indústria. A fim de aliviar os problemas computacionais, apresentamos uma abordagem probabilística baseada em diff para análise de mutação que reduz drasticamente o número de mutantes, omitindo linhas de código sem cobertura de instrução e linhas que são determinadas como desinteressantes - dublamos essas linhas áridas.
Estado do teste de mutação na Google

Para entender a fundo a estratégia adotada por essa companhia, leia a publicação de pesquisa sobre o estado do teste de mutação na Google, feita para a ocasião da 40ª Conferência Internacional de Engenharia de Software.


Verificando na prática

Para executar a cobertura de código e teste de mutação citados nesse texto, primeiramente clone esse repositório, executando:

git clone https://github.com/PauloGoncalvesBH/teste-de-mutacao.git
Enter fullscreen mode Exit fullscreen mode

Instale as dependências com o comando npm install.

Testes

O teste foi implementado utilizando jest. Para rodar os testes execute:

npm test
Enter fullscreen mode Exit fullscreen mode

Cobertura de código

Para rodar a cobertura de código, execute:

npm run test:coverage
Enter fullscreen mode Exit fullscreen mode

Teste de mutação

O teste de mutação é executado com a biblioteca stryker e com o runner do stryker para jest. Para rodar o teste de mutação execute:

npm run test:mutation
Enter fullscreen mode Exit fullscreen mode

Desafio

O que acha de aumentar o score do teste de mutação de 60% para 100%?

Crie novo teste no arquivo cnh.spec.js que mate 👿 as 2 mutações que estão sobrevivendo e mantenha a cobertura de código em 100%.


Fontes

Os seguintes materiais forneceram conteúdo e base para a criação desse texto:


Esse post está sendo versionado e hospedado no Github

Top comments (7)

Collapse
 
julissy profile image
Julissy Tocachelo

Excelente texto, estava confusa mas ficou esclarecido o assunto. Clonei o repositório e tentei executar os testes e deu erro. Se puder me ajudar, fico agradecida ^^ queria muito ver na prática esse teste

Collapse
 
paulogoncalvesr profile image
Paulo Gonçalves

Oi @julissy, que erro você teve? Consegue me passar ele pelo LinkedIn? (O dev.to não me notifica)

Collapse
 
emanuelgsouza profile image
Emanuel Gonçalves

Excelente texto.

Collapse
 
rafaabc profile image
rafaabc

Paulo Muito legal o artigo e parabéns!

Clonei o repositório e tentei criar os novos cenários, sendo =18 anos e >18 anos.

Após rodar os testes de mutação todos foram cobertos. Isso garante uma melhora na qualidade dos testes!! Obrigado por compartilhar!

Collapse
 
paulogoncalvesr profile image
Paulo Gonçalves • Edited

que ótimo que gostou do artigo e conseguiu ver na prática o teste de mutação funcionando @rafaabc :D

Collapse
 
carolciola profile image
Carol Ciola

Com base no seu artigo, consegui entender melhor este tipo de teste, e rodei meu primeiro teste de mutantes \o/ obrigada por compartilhar :)

Collapse
 
paulogoncalvesr profile image
Paulo Gonçalves

Que ótimo Carol :D feliz por ter colaborado contigo