Atingimos 100% de cobertura de testes! Será mesmo?
Coautor: Ernesto Barbosa 🚀
Quando falamos de qualidade de software, principalmente com ênfase em código, boa parte de nossa garantia provém do quanto cada parte é coberta por testes — sejam eles unitários, de integração ou de outras camadas. Porém, devido ao aumento de complexidade, regras e código — tanto de aplicação, quanto de testes — conforme o projeto aumenta, precisamos de ferramentas auxiliares para literalmente forçar erros no código enquanto executamos nossos testes, para assim, sabermos se estes estão capturando as falhas (conforme o esperado) ou deixando elas passarem — livres, leves e soltas.
A esta prática, damos o nome de testes de mutação.
Cobertura de código
Como funciona o processo de avaliação de cobertura de código (grande parte)
A métrica de cobertura de código avalia, considerando o número total de linhas de código do projeto, qual o quociente de total de linhas / total de linhas cobertas, ou em outras palavras, quanto do código é testado. Para obter esta métrica, as ferramentas de cobertura — grande maioria — executam os testes do projeto e, para cada teste executado, contabilizam os trechos de código invocados. Ao final da execução, é feito o cálculo e algumas delas geram relatórios ilustrando quais partes do código não foram chamadas.
Como funciona o processo de avaliação de cobertura de código mutante
Semelhante ao processo de cobertura de código comum, esta métrica avalia o quanto de código é testado. Entretanto, neste caso adicionamos uma variável a mais: mutações no código. Podemos organizar essas “mutações” em diferentes categorias, sendo: mutação de declarações de métodos, mutação de condições e mutação de valores.
Obs.: Vale lembrar que para linguagens de programação, existem ainda ferramentas que realizam mutações em diferentes níveis, como: class-level, considerando regras específicas de orientação a objetos (encapsulamento, herança, polimorfismo) e method-level, seguindo as três categorias citadas acima.
Para obter esta métrica, de forma lúdica, adotamos a nomenclatura de mutantes para os trechos de código que são modificados. Quanto maior a quantidade de mutantes mortos — ou seja, identificados pelos testes — mais eficiente é o teste e, consequentemente mais confiável é sua cobertura. Em contraponto, para cada mutante que sobrevive — ou seja, um trecho de código coberto por testes que passam mesmo havendo erros — é necessário uma avaliação e, possivelmente, uma refatoração dos testes relacionados até que estes passem a identificar (e matar, claro) os mutantes.
Ferramentas
A execução de testes de mutação é feita de forma automatizada. Para isso, utilizamos ferramentas que podem ser integradas ao projeto, realizar os testes e gerar os relatórios com o resultado das mutações / testes:
- Stryker Mutator (JS / C# / Scala)
- PIT Mutation Testing (Java)
- Mutant (Ruby)
- diversas outras…
Mão na massa! (Exemplo javascript)
Para o nosso exemplo, vamos demonstrar um passo a passo para implementação em uma aplicação javascript, que já possui alguns testes unitários, utilizando a biblioteca Stryker.
- Faça download deste projeto (zip ou git clone): https://github.com/samlucax/mutation-testing-with-stryker-javascript
- Instale as dependências:
npm install
- Instale o stryker-cli:
npm install -g stryker-cli
- Configure o stryker no projeto:
stryker init
Responda as perguntas que serão exibidas conforme abaixo:
? Are you using one of these frameworks? Then select a preset configuration. None/other
? Which test runner do you want to use? If your test runner isn’t listed here, you can choose “command” (it uses your `npm test` command, but will come with a big performance penalty) karma
? Which test framework do you want to use? jasmine
? What kind of code do you want to mutate? javascript
? [optional] What kind transformations should be applied to your code?
? Which reporter(s) do you want to use? html, clear-text, progress
? Which package manager do you want to use? npm
IMPORTANTE: Lembre-se que estas respostas servem para as tecnologias usadas no projeto de exemplo. Ao configurar para seu projeto, confira a documentação e responda de acordo com as suas tecnologias e preferências.
5 . Após concluir a configuração, invoque os mutantes: stryker run
6. Se tudo funcionou corretamente, você pode conferir um relatório html da execução na pasta reports/mutation
.
7. Se você quiser comparar o resultado dos testes de mutação com a cobertura de testes do projeto, execute npm run test
. Após isso, será gerado um relatório da cobertura de testes do projeto na pasta reports/coverage
.
Perceba que mesmo neste pequeno exemplo, para um projeto que possui 100% de cobertura de código, muitas falhas passam despercebidas: 47 mutações não foram identificadas pelos testes. 😥
Conclusão
Cuide de seus testes, e eles cuidarão de seu código.
As mutações são um aviso sobre nossos testes. Por isso, analise o resultado dos testes de mutações e aplique os ajustes necessários, seja no código de aplicação ou no de testes. O importante é ter garantias de que, se algo falhar — por mutante ou erro humano — o teste relacionado também vai falhar, e assim será possível realizar o ajuste.
Q & A:
Algumas perguntas feitas sobre o tema. Contribua!
Porque as ferramentas que medem cobertura de código, já não incluem testes de mutação (ou a capacidade de) para avaliar de fato se o código está “livre” de erros?
Em que momento do projeto então eu deveria incluir (ou executar) meus testes de mutação?
Devo executar os testes de mutação sempre para todo o projeto ou posso executar apenas para um contexto / feature específica?
Qual o impacto na cobertura de testes, quando ajustamos para matar os mutantes não identificados pelos testes? 300% de cobertura?
Testes de mutação servem apenas para testes unitários ou servem também para outras camadas de testes? Como integração, funcionais e de ponta a ponta?
É possível criar mutações customizadas ou somente as padrões?
Então quer dizer que estou fazendo Teste do Teste?
De forma geral, sim. É um paradoxo que parece um pouco estranho, eu sei. Mas se concordamos com o fato de que o código fonte deve ser testado, logo os testes unitários — que são parte do código fonte — também devem ser. Entretanto, testar todas as variações da aplicação enquanto executamos os nossos testes unitários é praticamente impossível se feito manualmente. Por isso, ferramentas de testes de mutação podem auxiliar a checar a confiabilidade dos nossos testes, e consequentemente, reduzir a chance de ter erros que passam despercebidos.
Veja / Leia também:
Leia também:
- https://pedrorijo.com/blog/intro-mutation/
- https://medium.com/appsflyer/tests-coverage-is-dead-long-live-mutation-testing-7fd61020330e
- https://www.researchgate.net/publication/221321980_Mutation_analysis_vs_code_coverage_in_automated_assessment_of_students'_testing_skills
- https://rachelcarmena.github.io/2017/09/01/do-we-have-a-good-safety-net-to-change-this-legacy-code.html
- https://www.guru99.com/mutation-testing.html
- https://www.researchgate.net/publication/310773886_Mutation_Testing_Techniques_A_Comparative_Study
- https://www.educba.com/mutation-testing/
- https://mutation.readthedocs.io/en/latest/