GithubHelp home page GithubHelp logo

paulogoncalvesbh / teste-de-mutacao Goto Github PK

View Code? Open in Web Editor NEW
31.0 2.0 5.0 133 KB

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

Home Page: https://dev.to/paulogoncalvesbh/testes-de-mutacao-1c7p

JavaScript 100.00%
mutation-testing code-coverage

teste-de-mutacao's Introduction

title published description tags canonical_url cover_image series
Teste de mutação 👽: O que é e como fica a cobertura de código?
true
Quem testa os testes? Como podemos garantir a eficiência dos testes? Conheça a resposta para essas perguntas
testes, ptbr, javascript

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)
})

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
}

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)
})

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

Instale as dependências com o comando npm install.

Testes

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

npm test

Cobertura de código

Para rodar a cobertura de código, execute:

npm run test:coverage

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

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

teste-de-mutacao's People

Contributors

paulogoncalvesbh avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.