Como usar o Stryker para mutation testing em projetos JavaScript

1. Introdução ao Mutation Testing e Stryker

O mutation testing é uma técnica de avaliação da qualidade de testes que vai muito além da simples cobertura de código. Enquanto a cobertura tradicional apenas verifica se uma linha foi executada, o mutation testing introduz pequenas alterações (mutações) no código fonte e verifica se os testes existentes são capazes de detectar essas mudanças.

No mutation testing, cada versão alterada do código é chamada de mutante. Quando um teste falha ao executar contra um mutante, dizemos que o mutante foi morto. Quando todos os testes passam mesmo com a mutação, o mutante sobreviveu. A taxa de mutação (mutation score) é a porcentagem de mutantes mortos em relação ao total.

O grande problema da cobertura de código tradicional é que ela pode mostrar 100% de cobertura mesmo quando os testes não verificam corretamente o comportamento do código. Por exemplo, um teste pode executar uma função de validação de CPF mas nunca verificar se ela realmente rejeita CPFs inválidos.

O Stryker Mutator é a principal ferramenta de mutation testing para ecossistemas JavaScript e TypeScript. Ele suporta múltiplos frameworks de teste (Jest, Mocha, Jasmine, Vitest) e oferece integração com ferramentas de CI/CD, além de gerar relatórios detalhados em HTML e dashboard.

2. Instalação e Configuração Inicial do Stryker

Para instalar o Stryker em seu projeto JavaScript, execute:

npm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner

Crie o arquivo de configuração stryker.conf.js na raiz do projeto:

// stryker.conf.js
module.exports = {
  mutate: ['src/**/*.js', '!src/**/*.test.js'],
  testRunner: 'jest',
  jest: {
    config: require('./jest.config.js')
  },
  reporters: ['progress', 'html', 'clear-text'],
  coverageAnalysis: 'perTest',
  concurrency: 4,
  thresholds: {
    high: 80,
    low: 60,
    break: 50
  }
};

Os parâmetros principais são:
- mutate: Define quais arquivos serão mutados (e quais ignorar)
- testRunner: Especifica o framework de testes (jest, mocha, jasmine)
- reporters: Define como os resultados serão exibidos
- coverageAnalysis: Pode ser 'off', 'perTest' ou 'all' — o modo 'perTest' é mais rápido pois só executa testes relevantes

3. Executando a Primeira Campanha de Mutation Testing

Vamos criar um exemplo prático com uma função simples de validação de CPF:

// src/validaCPF.js
function validaCPF(cpf) {
  cpf = cpf.replace(/\D/g, '');

  if (cpf.length !== 11) return false;
  if (/^(\d)\1{10}$/.test(cpf)) return false;

  let soma = 0;
  for (let i = 0; i < 9; i++) {
    soma += parseInt(cpf.charAt(i)) * (10 - i);
  }

  let resto = (soma * 10) % 11;
  if (resto === 10) resto = 0;
  if (resto !== parseInt(cpf.charAt(9))) return false;

  soma = 0;
  for (let i = 0; i < 10; i++) {
    soma += parseInt(cpf.charAt(i)) * (11 - i);
  }

  resto = (soma * 10) % 11;
  if (resto === 10) resto = 0;
  if (resto !== parseInt(cpf.charAt(10))) return false;

  return true;
}

module.exports = validaCPF;

Agora, execute o comando:

npx stryker run

O relatório inicial pode mostrar algo como:

[Survived] ConditionalExpression: 4 mutants survived
[Survived] BinaryOperator: 3 mutants survived
Mutation score: 72.34%

Isso indica que 27,66% dos mutantes sobreviveram — ou seja, há lacunas nos testes que não detectam alterações no comportamento da validação.

4. Análise de Mutantes Sobreviventes e Falsos Positivos

Ao analisar os mutantes sobreviventes, encontramos dois tipos:

Mutantes equivalentes: São mutações que produzem código funcionalmente idêntico ao original. Por exemplo, alterar if (resto === 10) resto = 0; para if (resto !== 10) resto = 0; muda a lógica, mas em alguns contextos pode não ser detectável.

Mutantes sobreviventes legítimos: Indicam que os testes não cobrem adequadamente o comportamento. Por exemplo, se a mutação altera return false para return true e o teste passa, significa que não há teste validando CPFs inválidos.

Para lidar com falsos positivos, podemos configurar exclusões:

// stryker.conf.js
module.exports = {
  // ... outras configurações
  mutator: {
    excludedMutations: ['StringLiteral', 'ObjectLiteral']
  },
  ignorePatterns: ['src/dados/**']
};

5. Otimizando a Performance do Stryker

O mutation testing pode ser computacionalmente caro. Para otimizar:

Paralelismo:

// stryker.conf.js
module.exports = {
  concurrency: 8,  // Ajuste conforme número de cores da CPU
  maxTestRunnerReuse: 10  // Reutiliza runners para evitar overhead
};

Escopo reduzido:

module.exports = {
  mutate: ['src/modulo-critico/**/*.js', '!src/modulo-critico/**/*.test.js']
};

Timeout adequado:

module.exports = {
  timeoutMS: 5000,  // 5 segundos por teste
  timeoutFactor: 1.5
};

6. Integração Contínua e Relatórios Avançados

Para integrar com GitHub Actions:

# .github/workflows/mutation-test.yml
name: Mutation Testing
on: [push, pull_request]
jobs:
  stryker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm ci
      - run: npx stryker run
        env:
          STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}

Para gerar relatórios HTML avançados:

module.exports = {
  reporters: ['html', 'dashboard', 'clear-text'],
  dashboard: {
    project: 'github.com/seu-usuario/seu-repo',
    version: 'main'
  }
};

Defina thresholds para bloquear o build se a qualidade cair:

module.exports = {
  thresholds: {
    high: 80,   // Meta desejável
    low: 60,    // Abaixo disso é alerta
    break: 50   // Abaixo disso bloqueia o build
  }
};

7. Boas Práticas e Armadilhas Comuns

Quando NÃO usar mutation testing:
- Projetos sem testes unitários (primeiro crie os testes)
- Bases de código muito grandes com tempo de execução excessivo (use escopo incremental)
- Código legado sem cobertura de testes mínima

Boas práticas:
- Comece com mutação em módulos críticos (validações, regras de negócio)
- Combine com testes de unidade, integração e regressão
- Use estratégia incremental: em PRs, mude apenas o código novo

Armadilhas comuns:
- Ignorar mutantes equivalentes pode levar a metas irreais
- Executar Stryker em toda a base de código sem filtro pode ser inviável
- Não configurar coverageAnalysis corretamente pode tornar a execução extremamente lenta

O Stryker é uma ferramenta poderosa para elevar a qualidade dos testes JavaScript. Quando usado corretamente, ele revela vulnerabilidades que a cobertura de código tradicional jamais mostraria.

Referências