Mutation Testing em JavaScript: Stryker e Qualidade Real da Suíte na Prática Já leu

O Que é Mutation Testing e Por Que Importa Mutation testing é uma técnica de avaliação de qualidade que vai além da simples cobertura de código. Enquanto testes convencionais medem apenas se você executou uma linha, mutation testing verifica se seus testes realmente conseguem detectar bugs. A ideia é simples e poderosa: introduzir pequenas mudanças (mutações) no código-fonte e verificar se seus testes falham. Se um teste não captura uma mutação óbvia, há uma brecha na qualidade real da sua suíte. Cobertura de 100% de linhas não significa cobertura de 100% de bugs. Um teste que apenas verifica se uma função é chamada, sem validar o resultado correto, criará uma falsa sensação de segurança. Mutation testing expõe essas vulnerabilidades. No JavaScript, a ferramenta mais madura para isso é o Stryker, que automatiza todo esse processo com inteligência e performance. Como Funciona o Stryker em Prática Instalação e Configuração Inicial Instalar o Stryker é direto. Você precisa do framework de testes

O Que é Mutation Testing e Por Que Importa

Mutation testing é uma técnica de avaliação de qualidade que vai além da simples cobertura de código. Enquanto testes convencionais medem apenas se você executou uma linha, mutation testing verifica se seus testes realmente conseguem detectar bugs. A ideia é simples e poderosa: introduzir pequenas mudanças (mutações) no código-fonte e verificar se seus testes falham. Se um teste não captura uma mutação óbvia, há uma brecha na qualidade real da sua suíte.

Cobertura de 100% de linhas não significa cobertura de 100% de bugs. Um teste que apenas verifica se uma função é chamada, sem validar o resultado correto, criará uma falsa sensação de segurança. Mutation testing expõe essas vulnerabilidades. No JavaScript, a ferramenta mais madura para isso é o Stryker, que automatiza todo esse processo com inteligência e performance.

Como Funciona o Stryker em Prática

Instalação e Configuração Inicial

Instalar o Stryker é direto. Você precisa do framework de testes (Jest, Mocha, etc.) já funcionando:

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

O comando init gera um stryker.conf.js com padrões sensatos. Aqui está uma configuração mínima para um projeto com Jest:

// stryker.conf.js
export default {
  testRunner: 'jest',
  testPathPattern: './src/**/*.test.js',
  mutate: ['src/**/*.js', '!src/**/*.test.js', '!src/**/*.spec.js'],
  reporters: ['html', 'clear-text', 'progress'],
  timeoutMS: 5000,
  concurrency: 4
};

O mutate define quais arquivos sofrem mutações. O timeoutMS evita testes presos em loops infinitos após uma mutação causar impasse. Com concurrency: 4, o Stryker executa 4 testes em paralelo, acelerando drasticamente a análise.

Executando e Interpretando Resultados

Execute com npx stryker run. O Stryker testará centenas de variações do seu código. Cada mutação tem um status:

  • Killed: seu teste detectou a mutação (bom!)
  • Survived: a mutação não foi detectada (problema!)
  • Timeout: a mutação causou loop infinito
  • Ignored: você marcou para ignorar

A métrica principal é a mutation score: percentual de mutações mortas. Uma suíte com score 80%+ é considerada robusta. O relatório HTML mostra exatamente onde estão as lacunas.

Exemplo Real: Testando uma Função de Validação

Código Original com Teste Inadequado

// src/validator.js
export function validateEmail(email) {
  if (email.includes('@')) {
    return true;
  }
  return false;
}

// src/validator.test.js (RUIM)
test('validateEmail returns true for email with @', () => {
  expect(validateEmail('user@example.com')).toBe(true);
});

Este teste tem cobertura 100% de linhas, mas é fraco. O Stryker criará mutações como email.includes('@')email.includes('#'), e seu teste não falhará. Mutation score: baixo.

Teste Adequado Que Mata Mutações

// src/validator.test.js (BOM)
describe('validateEmail', () => {
  test('returns true for valid email', () => {
    expect(validateEmail('user@example.com')).toBe(true);
  });

  test('returns false for email without @', () => {
    expect(validateEmail('userexample.com')).toBe(false);
  });

  test('returns false for empty string', () => {
    expect(validateEmail('')).toBe(false);
  });

  test('returns false for @ only', () => {
    expect(validateEmail('@')).toBe(false);
  });
});

Agora temos limites claros. Mutações como trocar includes('@') por includes('#'), ou true por false, serão capturadas. O mutation score sobe significativamente.

Lição crítica: Testes efetivos testam casos positivos E negativos, com asserções específicas sobre o resultado esperado.

Estratégias para Melhorar Mutation Score

Cobertura de Condições Booleanas

Mutation testing excelente exige testar cada caminho através de condições. Para um operador AND, teste quando ambas as partes são verdadeiras E quando cada uma é falsa:

// src/permission.js
export function canEdit(user) {
  return user.isAdmin || user.isOwner;
}

// Testes que cobrem todas as combinações booleanas
test('returns true if user is admin', () => {
  expect(canEdit({ isAdmin: true, isOwner: false })).toBe(true);
});

test('returns true if user is owner', () => {
  expect(canEdit({ isAdmin: false, isOwner: true })).toBe(true);
});

test('returns false if neither admin nor owner', () => {
  expect(canEdit({ isAdmin: false, isOwner: false })).toBe(false);
});

test('returns true if both admin and owner', () => {
  expect(canEdit({ isAdmin: true, isOwner: true })).toBe(true);
});

O Stryker criará mutações trocando || por &&. Sem esses testes complementares, as mutações sobreviveriam.

Validação de Valores Retornados

Não confie apenas em tipo. Valide valores reais:

// src/calculator.js
export function add(a, b) {
  return a + b;
}

// Fraco
test('add returns a number', () => {
  expect(typeof add(2, 3)).toBe('number');
});

// Forte
test('add returns correct sum', () => {
  expect(add(2, 3)).toBe(5);
  expect(add(-1, 1)).toBe(0);
  expect(add(0, 0)).toBe(0);
});

A primeira versão não mata a mutação a + ba - b. A segunda mata.

Observando o Relatório HTML

Após npx stryker run, abra o arquivo gerado em reports/mutation/index.html. Ele destaca em vermelho cada linha com mutações sobreviventes. Isso guia sua escrita de testes de forma cirúrgica. Você vê exatamente onde adicionar cobertura.

Conclusão

Mutation testing com Stryker transformou minha forma de avaliar qualidade real de código. Três aprendizados fundamentais: (1) Cobertura de linhas é métrica enganosa — mutation score revela vulnerabilidades reais; (2) Testes robustos precisam de casos positivos, negativos e limites, com asserções específicas sobre valores, não apenas tipos; (3) O relatório HTML do Stryker é uma ferramenta de navegação que economiza horas de análise manual.

Implemente isso em seu próximo projeto. Comece com stryker init, veja o mutation score inicial e persiga melhorias incrementais. A experiência é reveladora.

Referências


Artigos relacionados