DEV Community

Lucas Pereira de Souza
Lucas Pereira de Souza

Posted on

Mutation Testing com Stryker

logotech

## Mutantes no Código: Como o Stryker Multator Aumenta a Qualidade do Seu Backend

Você já parou para pensar o quão confiáveis são seus testes? Em um mundo onde a qualidade do software é cada vez mais crucial, especialmente em sistemas backend, confiar cegamente na cobertura de código pode ser uma armadilha. É aqui que entram os testes mutantes, uma técnica poderosa para ir além da superfície e garantir que seus testes realmente capturem as falhas que importam. Neste post, vamos desmistificar os testes mutantes, aprender a configurar o Stryker Mutator em um projeto TypeScript/Node.js e, o mais importante, interpretar os relatórios gerados para elevar a qualidade do seu código.

O Problema: A Falsa Sensação de Segurança da Cobertura de Código

Testes automatizados são a espinha dorsal de qualquer projeto de software robusto. Eles nos dão confiança para refatorar, adicionar novas funcionalidades e, em geral, manter a sanidade em meio à complexidade crescente. No entanto, a métrica tradicional de cobertura de código, embora útil, pode ser enganosa. Um teste pode \"cobrir\" uma linha de código sem, de fato, verificar seu comportamento corretamente. Isso significa que uma alteração sutil, mas crítica, pode passar despercebida, levando a bugs em produção.

Imagine um teste que verifica se um número é maior que 10. Se a condição for alterada para \"maior que 5\", a cobertura de código pode permanecer em 100%, mas o teste deixará de detectar um erro caso o valor esperado fosse, na verdade, 8. Os testes mutantes surgem como uma solução para esse problema.

Testes Mutantes: Introduzindo o Inimigo para Vencer

Testes mutantes são uma técnica que modifica automaticamente pequenas partes do seu código-fonte (gerando \"mutantes\") e, em seguida, executa seus testes existentes para ver se eles detectam essas alterações. Se um teste falhar após uma modificação, o mutante é considerado \"morto\", indicando que seu teste foi eficaz em detectar essa mudança. Se nenhum teste falhar, o mutante \"sobrevive\", sinalizando uma potencial fraqueza em sua suíte de testes.

O processo geralmente segue estes passos:

  1. Geração de Mutantes: Ferramentas como o Stryker Mutator introduzem pequenas alterações sintáticas no seu código (ex: trocar > por <, + por -, true por false, remover return, etc.).
  2. Execução dos Testes: Seus testes existentes são executados contra cada versão modificada do código (cada mutante).
  3. Análise dos Resultados:
    • Mutante Morto: Se pelo menos um teste falhar, o mutante é considerado morto. Isso é bom, pois significa que seus testes pegaram a alteração.
    • Mutante Sobrevivente: Se todos os testes passarem, o mutante sobreviveu. Isso é um alerta, indicando que seus testes não são sensíveis o suficiente para essa modificação específica.

Configurando o Stryker Mutator com TypeScript/Node.js

Vamos colocar a mão na massa! Assumiremos que você já tem um projeto Node.js com TypeScript e uma suíte de testes configurada (por exemplo, com Jest).

1. Instalação:

Primeiro, instale o Stryker Mutator e os plugins necessários como dependências de desenvolvimento:

npm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner @stryker-mutator/typescript
Enter fullscreen mode Exit fullscreen mode

2. Configuração Inicial (stryker.conf.json):

Crie um arquivo stryker.conf.json na raiz do seu projeto. Este arquivo instrui o Stryker sobre como operar.

// stryker.conf.json
{
  \"$schema\": \"./node_modules/@stryker-mutator/core/schema/stryker-schema.json\",
  \"mutate\": [
    \"src/**/*.ts\" // Caminho para os arquivos que você quer mutar (ex: todo o código fonte em 'src')
  ],
  \"testRunner\": \"jest\", // O test runner que você está usando
  \"reporters\": [
    \"clear-text\", // Relatório legível no console
    \"html\" // Relatório HTML interativo
  ],
  \"htmlReporter\": {
    \"baseDir\": \"stryker-report\" // Diretório onde o relatório HTML será gerado
  },
  \"jest\": {
    // Configurações específicas do Jest, se necessário.
    // Por exemplo, para especificar o arquivo de configuração do Jest:
    // \"config\": \"jest.config.js\"
  },
  \"thresholds\": {
    \"high\": 80, // Mutants killed % above which the build is considered successful
    \"low\": 60,  // Mutants killed % above which the build is considered acceptable
    \"break\": 60 // Mutants killed % below which the build should break
  },
  \"plugins\": [
    \"@stryker-mutator/jest-runner\",
    \"@stryker-mutator/typescript\"
  ],
  \"tempDirName\": \".temp-stryker\", // Diretório temporário para mutantes
  \"cleanTempDir\": true // Limpar o diretório temporário após a execução
}
Enter fullscreen mode Exit fullscreen mode

Explicação dos Campos Principais:

  • mutate: Define quais arquivos e diretórios o Stryker deve analisar para criar mutantes.
  • testRunner: Informa ao Stryker qual test runner usar (Jest, Mocha, etc.).
  • reporters: Especifica os formatos de relatório. clear-text é ótimo para o console, html gera um relatório interativo.
  • htmlReporter: Configura o diretório de saída para o relatório HTML.
  • jest: Permite passar configurações adicionais para o Jest.
  • thresholds: Define metas percentuais para a taxa de mutantes mortos. break é crucial para CI/CD, pois pode falhar a build se a meta não for atingida.
  • plugins: Carrega os plugins necessários para seu test runner e linguagem.

3. Executando o Stryker:

No seu terminal, execute o comando:

npx stryker run
Enter fullscreen mode Exit fullscreen mode

O Stryker irá:

  • Ler sua configuração.
  • Identificar os arquivos a serem mutados.
  • Gerar mutantes para cada arquivo.
  • Executar seus testes contra cada mutante.
  • Gerar os relatórios conforme configurado.

Exemplo de Código e Testes (TypeScript/Node.js):

Vamos considerar um exemplo simples de uma função de cálculo em um arquivo src/calculator.ts:

// src/calculator.ts

/**
 * Adiciona dois números.
 * @param a O primeiro número.
 * @param b O segundo número.
 * @returns A soma de a e b.
 */
export function add(a: number, b: number): number {
  // Se um mutante trocar '+' por '-', este teste deve matá-lo.
  return a + b;
}

/**
 * Verifica se um número é positivo.
 * @param num O número a ser verificado.
 * @returns True se o número for positivo, False caso contrário.
 */
export function isPositive(num: number): boolean {
  // Se um mutante trocar '>=' por '>', este teste deve matá-lo.
  return num >= 0;
}
Enter fullscreen mode Exit fullscreen mode

E seus testes correspondentes em src/calculator.test.ts (usando Jest):

// src/calculator.test.ts
import { add, isPositive } from './calculator';

describe('Calculator', () => {
  describe('add', () => {
    it('should return the sum of two positive numbers', () => {
      expect(add(2, 3)).toBe(5);
    });

    it('should return the sum of a positive and a negative number', () => {
      expect(add(5, -2)).toBe(3);
    });

    it('should return the sum when one number is zero', () => {
      expect(add(0, 7)).toBe(7);
    });

    // Teste para garantir que o mutante que muda '+' para '-' seja pego
    it('should correctly add negative numbers', () => {
      expect(add(-1, -2)).toBe(-3);
    });
  });

  describe('isPositive', () => {
    it('should return true for a positive number', () => {
      expect(isPositive(10)).toBe(true);
    });

    it('should return true for zero', () => {
      // Se um mutante trocar '>=' por '>', este teste deve matá-lo.
      expect(isPositive(0)).toBe(true);
    });

    it('should return false for a negative number', () => {
      expect(isPositive(-5)).toBe(false);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Se você rodar npx stryker run com este código, o Stryker criará mutantes como:

  • return a - b; (na função add)
  • return a * b; (na função add)
  • return num > 0; (na função isPositive)

Seus testes atuais são bons o suficiente para matar a maioria desses mutantes, mas o Stryker irá nos dizer exatamente quais sobreviveram.

Interpretando o Relatório de Mutação

Após a execução, o Stryker apresentará um relatório no console e, se configurado, um relatório HTML interativo.

Relatório do Console (Exemplo Simplificado):

Stryker: Running tests...
Stryker: Detected 2 files to mutate.
Stryker: Running tests against 2 mutants.
... (progresso) ...
Stryker: All tests passed on the original code.

----------------------------------- Mutant test results ----------------------------------
| Status        | Count | Percentage | Description                                              |
|---------------|-------|------------|----------------------------------------------------------|
| Killed        | 15    | 75%        | Tests failed on this mutant.                             |
| Alive         | 5     | 25%        | All tests passed on this mutant.                         |
| Pending       | 0     | 0%         | Not tested due to timeout or other reasons.              |
| Ignored       | 0     | 0%         | Mutant was ignored based on stryker configuration.       |
| Total         | 20    | 100%       |                                                          |
------------------------------------------------------------------------------------------
Mutation Score: 75%
Enter fullscreen mode Exit fullscreen mode

Pontos Chave para Interpretação:

  • Mutation Score: A métrica mais importante. É a porcentagem de mutantes que foram \"mortos\" pelos seus testes. Um score alto indica que seus testes são eficazes em detectar alterações.
  • Killed vs. Alive:
    • Killed (Mortos): Ótimo! Seus testes detectaram a falha introduzida pelo mutante.
    • Alive (Vivos): Alerta vermelho! Seus testes não falharam mesmo com a alteração no código. Isso sugere que você precisa adicionar ou melhorar seus testes para cobrir esse cenário específico.
  • Relatório HTML: O relatório HTML (gerado no diretório stryker-report por padrão) é extremamente útil. Ele oferece uma visão detalhada:
    • Lista todos os mutantes.
    • Mostra o status de cada um (Killed, Alive, etc.).
    • Permite clicar em um mutante \"Alive" para ver exatamente qual parte do código foi modificada e quais testes passaram. Isso direciona seus esforços para onde eles são mais necessários.
    • Visualiza a cobertura de testes em nível de mutante.

O Que Fazer com Mutantes Vivos?

  1. Melhore seus Testes: O cenário mais comum é que um mutante vivo indica uma falha na sua suíte de testes. Você precisa adicionar asserções ou casos de teste que cubram a condição alterada pelo mutante. Por exemplo, se o mutante return num > 0; sobreviveu no nosso isPositive, significa que nenhum teste verificou o comportamento exato quando o número é positivo (sem ser zero). Adicionar um teste como expect(isPositive(5)).toBe(true); mataria esse mutante.
  2. Ajuste a Configuração do Stryker: Em alguns casos raros, um mutante vivo pode ser aceitável. Talvez a mutação introduza um bug trivial ou uma condição que nunca deveria ocorrer em produção. Você pode configurar o Stryker para ignorar certos arquivos ou mutações específicas usando ignorePatterns no stryker.conf.json. Use isso com cautela!

Conclusão: Rumo a um Código Mais Confiável

Testes mutantes, especialmente com ferramentas como o Stryker Mutator, são uma evolução natural e necessária quando buscamos a excelência em qualidade de software. Eles nos forçam a pensar criticamente sobre nossos testes e a garantir que eles não apenas executem, mas que validem o comportamento correto do nosso código.

Ao integrar o Stryker no seu fluxo de desenvolvimento e CI/CD, você adiciona uma camada robusta de validação, aumentando significativamente a confiança na sua base de código e prevenindo bugs sutis que poderiam passar despercebidos. Não se contente com a cobertura de código; abrace os testes mutantes e construa backends verdadeiramente robustos e confiáveis.

Top comments (0)