Tem certeza de que seus testes realmente funcionam?
\n\nImagine: você tem 95% de cobertura de código, todos os testes estão verdes, o CI/CD passa sem erros. A equipe está satisfeita, o gerente está tranquilo. Mas um dia, um bug aparece em produção. Você abre o código, olha para os testes e percebe: eles apenas verificam se a função retorna um número, mas não verificam o valor dele. Ou pior — o teste foi escrito de forma que passa em qualquer cenário.
\n\nSituação familiar? Bem-vindo ao mundo onde a porcentagem de cobertura é apenas um número, não uma garantia de qualidade. É aqui que entra o teste de mutação (mutation testing) — uma metodologia que realmente faz seus testes sofrerem e provarem seu valor.
\n\nNeste artigo, vamos explorar como implementar o mutation testing em um projeto TypeScript usando Stryker, e por que isso pode se tornar sua nova ferramenta favorita (ou uma nova fonte de dor de cabeça).
\n\nO que é teste de mutação em palavras simples?
\n\nTeste de mutação é quando você pega seu código, introduz intencionalmente pequenos erros (mutações) e verifica se seus testes falham. Se os testes não falham — significa que você está testando incorretamente. Se falham — você está de parabéns, mas o mutante foi eliminado.
\n\nParece sádico? Talvez. Mas funciona. Imagine que você é um engenheiro de QA que secretamente entrou no código e substituiu === por ==, > por < ou removeu uma chamada de função. Um bom teste deve perceber isso. Um teste ruim vai deixar passar.
Em TypeScript, com sua tipagem forte, o teste de mutação é especialmente útil: ele captura casos onde os tipos estão corretos, mas a lógica não está.
\n\nPor que a cobertura de código comum (code coverage) é uma ilusão?
\n\nSuponha que você tenha uma função:
\n\nfunction calculateDiscount(price: number, isPremium: boolean): number {\n if (isPremium) {\n return price * 0.8;\n }\n return price;\n}\n\nE um teste:
\n\ntest('returns number', () => {\n const result = calculateDiscount(100, true);\n expect(typeof result).toBe('number');\n});\n\nCobertura? 100%. Teste? Passa. E se substituirmos price * 0.8 por price * 0.5? O teste ainda passará — ele só verifica o tipo, não o valor. Aqui está a falsa sensação de segurança.
O teste de mutação destacará essa mutação e dirá: "Ei, seu teste não percebeu que eu mudei o desconto!". E você terá que escrever uma verificação adequada.
\n\nComo o mutation testing funciona em TypeScript: passo a passo
\n\nTodo o processo pode ser dividido em três etapas:
\n\n- \n
- Geração de mutantes — a ferramenta copia seu código e faz uma alteração de cada vez. Por exemplo, troca
&&por||ou remove uma chamada de método. \n - Execução dos testes — para cada mutante, seus testes são executados. \n
- Análise dos resultados — se os testes falharam, o mutante foi "morto". Se não, ele "sobreviveu", e isso é uma zona de risco. \n
A principal métrica é o mutation score (porcentagem de mutantes mortos). 100% é o ideal, mas na prática, 80-90% já é excelente.
\n\nFerramenta nº 1 para TypeScript: Stryker
\n\nNo mundo TypeScript e JavaScript, o rei do teste de mutação é o Stryker (StrykerJS). Ele é rápido, funciona com Jest, Vitest, Mocha e até com testes de navegador. Configura-se em 5 minutos.
\n\nVamos instalar:
\n\nnpm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner\n\nCriaremos o arquivo de configuração stryker.conf.js:
// stryker.conf.js\nexport default {\n mutate: ['src/**/*.ts', '!src/**/*.spec.ts'], // o que mutar\n testRunner: 'jest',\n reporters: ['html', 'clear-text', 'progress'],\n jest: {\n projectType: 'custom',\n configFile: 'jest.config.ts',\n },\n concurrency: 4, // quantos mutantes