├── src └── cnh.js ├── .gitignore ├── __tests__ └── cnh.spec.js ├── stryker.conf.js ├── package.json └── README.md /src/cnh.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = podeTirarCarteiraDeMotorista = idade => { 3 | return idade >= 18 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | reports/ 4 | 5 | # stryker temp files 6 | .stryker-tmp 7 | stryker.log 8 | -------------------------------------------------------------------------------- /__tests__/cnh.spec.js: -------------------------------------------------------------------------------- 1 | const podeTirarCarteiraDeMotorista = require('../src/cnh') 2 | 3 | test('Deve retornar false para pessoa com menos de 18 anos', () => { 4 | expect(podeTirarCarteiraDeMotorista(17)).toBe(false) 5 | }) 6 | -------------------------------------------------------------------------------- /stryker.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@stryker-mutator/api/core').StrykerOptions} 3 | */ 4 | module.exports = { 5 | mutate: [ 6 | 'src/cnh.js' 7 | ], 8 | reporters: ['html', 'clear-text'], 9 | testRunner: 'jest', 10 | jest: { 11 | enableFindRelatedTests: false, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "repository": { 4 | "type": "git", 5 | "url": "https://github.com/PauloGoncalvesBH/Testes-de-mutacao-em-JS.git" 6 | }, 7 | "dependencies": { 8 | "@stryker-mutator/core": "4.1.2", 9 | "@stryker-mutator/jest-runner": "^4.1.2", 10 | "jest": "^26.6.3" 11 | }, 12 | "scripts": { 13 | "test": "jest", 14 | "test:coverage": "jest --collectCoverage true", 15 | "test:mutation": "stryker run" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Teste de mutação 👽: O que é e como fica a cobertura de código?" 3 | published: true 4 | description: "Quem testa os testes? Como podemos garantir a eficiência dos testes? Conheça a resposta para essas perguntas" 5 | tags: testes, ptbr, javascript 6 | canonical_url: 7 | cover_image: 8 | series: 9 | --- 10 | 11 | ## Sumário 12 | - [Who tests the tests?](#who-tests-the-tests) 13 | - [Porque cobertura de código não é confiável](#porque-cobertura-de-código-não-é-confiável) 14 | - [Testes de mutação](#testes-de-mutação) 15 | - [Detalhe da execução](#detalhe-da-execução) 16 | - [RIP Cobertura de código?](#rip-cobertura-de-código) 17 | - [Desvantagem](#desvantagem) 18 | - [Adoção em grandes projetos - Case Google](#adoção-em-grandes-projetos---case-google) 19 | - [Verificando na prática](#verificando-na-prática) 20 | - [Cobertura de código](#cobertura-de-código) 21 | - [Teste de mutação](#teste-de-mutação) 22 | - [Fontes](#fontes) 23 | 24 | --- 25 | 26 | _Esse conteúdo foi apresentado com mais detalhes e profundidade na live do AT Talks em 25/11/20. [Para assistir clique aqui.](https://www.youtube.com/watch?v=TIiVYhoEB8o)_ 27 | 28 |

Who tests the tests?

29 | 30 | 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? 31 | 32 | > Cobertura de código é uma métrica que valida o quanto do código foi coberto pelos testes. 33 | > Ou seja, verifica quais linhas do código foram executadas ao rodar os testes e retorna o percentual de cobertura. 34 | > 35 | > _Leia esse [conteúdo (en)](https://en.wikipedia.org/wiki/Code_coverage) e [esse (pt-br)](https://medium.com/liferay-engineering-brazil/um-pouco-sobre-cobertura-de-c%C3%B3digo-e-cobertura-de-testes-4fd062e91007) para saber mais._ 36 | 37 | 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 😮. 38 | 39 | 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. 40 | 41 | 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%. 42 | 43 | O problema é que, como dito, a cobertura de código não valida a eficiência dos testes, e vamos ver abaixo o porquê. 44 | 45 | --- 46 | 47 |

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

48 | 49 | Abaixo temos um pequeno método que possui apenas 1 teste validando o seu comportamento. 50 | 51 | > _**Disclaimer**_ 52 | > É 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_. 53 | 54 | ```js 55 | // ./src/cnh.js 56 | module.exports = podeTirarCarteiraDeMotorista = idade => { 57 | return idade >= 18 58 | } 59 | 60 | // ./__tests__/cnh.spec.js 61 | test('Deve retornar false para pessoa com menos de 18 anos', () => { 62 | expect(podeTirarCarteiraDeMotorista(17)).toBe(false) 63 | }) 64 | ``` 65 | 66 | 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: 67 | 1. Deve retornar `true` se idade for igual a `18` 68 | 1. Deve retornar `true` se idade for igual a `19` 69 | 70 | #### Cobertura de código de `cnh.js`: 71 | 72 | ![Print da cobertura de código mostrando score de 100%](https://user-images.githubusercontent.com/29241659/100894080-dc443e80-349a-11eb-9649-c7db04d27626.png) 73 | 74 | E é baseado nessa brecha da métrica de linhas executadas é que o uso do teste de mutação faz sentido. 75 | 76 | > "...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" 77 | > _Baeldung, 2018_ 78 | 79 | --- 80 | 81 |

Testes de mutação

82 | 83 | > 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. 84 | 85 | O conceito de teste de mutação é bem simples: 86 | 87 | 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. 88 | 89 | Ainda não está claro? Então vamos lá. 90 | 91 | Abaixo está o nosso código original: 92 | 93 | ```js 94 | // ./src/cnh.js 95 | const podeTirarCarteiraDeMotorista = idade => { 96 | return idade >= 18 97 | } 98 | ``` 99 | 100 | 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): 101 | - A expressão condicional `idade >= 18` será alterada para `true` e `false`; 102 | - O operador de idade `>=` será alterado para `<` e `>`; 103 | - O bloco `=> { return idade >= 18 }` será alterado para `=> {}`. 104 | 105 | > Quer entender tudo que será mutado no seu código e para o que será mutado? Leia '[Mutantes suportados pelo stryker (en)](https://github.com/stryker-mutator/stryker-handbook/blob/master/mutator-types.md)'. 106 | 107 | 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. 108 | 109 | É 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. 110 | 111 | > 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" 112 | 113 | Se executarmos teste de mutação utilizando o teste e código apresentados anteriormente, o resultado seria esse: 114 | 115 | ![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](https://user-images.githubusercontent.com/29241659/100894680-82904400-349b-11eb-8a3e-d3892cd09236.png) 116 | 117 | 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. 118 | 119 | Para que todos os 5 mutantes não sobrevivam, precisamos criar um novo teste que cubra essa brecha, como: 120 | 121 | ```js 122 | test('Deve retornar true para pessoa maior de 18 anos', () => { 123 | expect(podeTirarCarteiraDeMotorista(18)).toBe(true) 124 | }) 125 | ``` 126 | 127 |

Detalhe da execução

128 | 129 | Quando executamos o teste de mutação são feitas as seguintes etapas: 130 | 1. Analisa quais arquivos serão mutados; 131 | - No nosso caso foi `cnh.js`. 132 | 1. Executa todos os testes e espera que todos passem; 133 | - 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. 134 | 1. Gera mutante para todos os trechos de código; 135 | - No nosso caso foram 5 mutantes criados. 136 | 1. Executa todos os testes para cada mutante gerado; 137 | 1. 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. 138 | 139 | 140 |

RIP Cobertura de código?

141 | 142 | 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. 143 | 144 | Portanto, cobertura de código continuará sendo uma métrica bastante usada e não é uma ferramenta antagonista ao teste de mutação 145 | 146 |

Desvantagem

147 | 148 | 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. 149 | 150 | 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. 151 | 152 | Um exemplo dessa limitação é o projeto [ServeRest](https://github.com/PauloGoncalvesBH/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. 153 | 154 | > 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. 155 | 156 |

Adoção em grandes projetos - Case Google

157 | 158 | >> **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](https://www.youtube.com/watch?v=TIiVYhoEB8o).** 159 | 160 | 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](https://dl.acm.org/doi/pdf/10.1145/2854146)), porém ela teve que utilizar de algumas estratégias de criação da mutação. 161 | 162 | > 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. 163 | > _State of Mutation Testing at Google_ 164 | 165 | No bom português: 166 | 167 | > 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. 168 | > _Estado do teste de mutação na Google_ 169 | 170 | 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](https://research.google/pubs/pub46584/), feita para a ocasião da _40ª Conferência Internacional de Engenharia de Software_. 171 | 172 | --- 173 | 174 |

Verificando na prática

175 | 176 | Para executar a cobertura de código e teste de mutação citados nesse texto, primeiramente clone [esse repositório](https://github.com/PauloGoncalvesBH/teste-de-mutacao), executando: 177 | 178 | ```sh 179 | git clone https://github.com/PauloGoncalvesBH/teste-de-mutacao.git 180 | ``` 181 | 182 | Instale as dependências com o comando `npm install`. 183 | 184 |

Testes

185 | 186 | O teste foi implementado utilizando [jest](https://www.npmjs.com/package/jest). Para rodar os testes execute: 187 | 188 | ```sh 189 | npm test 190 | ``` 191 | 192 |

Cobertura de código

193 | 194 | Para rodar a cobertura de código, execute: 195 | 196 | ```sh 197 | npm run test:coverage 198 | ``` 199 | 200 |

Teste de mutação

201 | 202 | O teste de mutação é executado com a biblioteca [stryker](https://www.npmjs.com/package/@stryker-mutator/core) e com o runner do [stryker para jest](https://www.npmjs.com/package/@stryker-mutator/jest-runner). Para rodar o teste de mutação execute: 203 | 204 | ```sh 205 | npm run test:mutation 206 | ``` 207 | 208 | #### Desafio 209 | 210 | O que acha de aumentar o score do teste de mutação de _60%_ para _100%_? 211 | 212 | Crie novo teste no arquivo [cnh.spec.js](/__tests__/cnh.spec.js) que mate 👿 as 2 mutações que estão sobrevivendo e mantenha a cobertura de código em _100%_. 213 | 214 | --- 215 | 216 |

Fontes

217 | 218 | Os seguintes materiais forneceram conteúdo e base para a criação desse texto: 219 | 220 | - [State of Mutation Testing at Google](https://research.google/pubs/pub46584/) 221 | - [Mutation Testing - Wikipedia](https://en.wikipedia.org/wiki/Mutation_testing) 222 | - [Apresentação 'An intro to mutation testing - or why coverage sucks'](https://speakerdeck.com/pedrorijo91/mutation-testing-pixels-camp-2019) 223 | - [Mutantes suportados pelo Stryker](https://github.com/stryker-mutator/stryker-handbook/blob/master/mutator-types.md) 224 | - [Mutation Testing: What It Is and How It Makes Code Coverage Matter](https://dev.to/carlosschults/mutation-testing-what-it-is-and-how-it-makes-code-coverage-matter-ijp) 225 | - [Code coverage is useless](https://dev.to/johnpreese/code-coverage-is-useless-1h3h) 226 | - [Why code coverage is not a reliable metric](https://dev.to/conectionist/why-code-coverage-is-not-a-reliable-metric-327l) 227 | - [Mutation testing in 1000 characters](https://dev.to/schreiber_chris/mutation-testing-in-1000-characters-193a) 228 | - [Why Google Stores Billions of Lines of Code in a Single Repository](https://dl.acm.org/doi/pdf/10.1145/2854146) 229 | 230 | --- 231 | 232 | ##### _Esse post está sendo versionado e hospedado no [Github](https://github.com/PauloGoncalvesBH/teste-de-mutacao)_ 233 | --------------------------------------------------------------------------------