Fala, dev! Tudo certo? 🚀
Chegamos ao quinto capítulo da nossa série Testes para Dev!
Nos artigos anteriores aprendemos sobre Testes de Unidade, Testes de Integração, Continuous Integration (CI/CD) e Testes de Contrato.
Agora, imagine o seguinte cenário no mundo real:
Um desenvolvedor orgulhoso abre o relatório de cobertura do Jest/Istanbul e mostra para a equipe: "Consegui 100% de Code Coverage! Nenhuma linha de código ficou sem testar!". O Tech Lead aprova o Pull Request. No dia seguinte em produção, uma regra de desconto dá abatimento total para uma compra de valor exatamente igual a R$ 100, quando deveria conceder desconto apenas em valores estritamente superiores a R$ 100. A empresa perde milhares de reais em minutos por conta de um erro de limite (
>=em vez de>). O relatório de cobertura marcou a linha como 100% executada... KABOOM! 💥 A empresa perdeu dinheiro com 100% de cobertura verde!
Como um teste pode passar por 100% das linhas e mesmo assim não pegar um erro crítico desses?
Para resolver essa falsa sensação de segurança e responder à clássica pergunta "Quem testa os testes?", surgem os Testes de Mutação.
Hoje vamos aprender passo a passo (baby steps) como instalar, configurar o Stryker Mutator, entender cada termo técnico, criar testes que realmente protegem a aplicação, ler o relatório HTML de mutantes e integrar os testes de mutação no pipeline de CI/CD sem travar a velocidade do time.
O que é um Teste de Mutação? 🧬
Em termos simples: é um teste que inspeciona a qualidade dos seus testes automatizados injetando pequenos bugs (mutações) propositais no código-fonte.
Em vez de verificar se o seu código funciona, a ferramenta de mutação (como o Stryker) altera o seu código de propósito e roda os seus testes para ver se eles são capazes de notar a alteração e falhar.
Glossário de Termos dos Testes de Mutação 📖
Para não ficar perdido durante a leitura, guarde este pequeno dicionário de termos:
- Mutante (Mutant): Uma versão levemente modificada do seu código-fonte gerada pela ferramenta.
- Operador de Mutação (Mutation Operator): A regra lógica usada para alterar o código (ex: trocar
>=por>, trocar+por-, trocartrueporfalse, remover o conteúdo de umreturn). - Mutante Morto (Killed Mutant) 🟢: A ferramenta alterou o código, seus testes rodaram e pelo menos um teste falhou. Isso é excelente! Significa que seu teste percebeu o erro e está atento.
- Mutante Sobrevivente (Survived Mutant) 🛑: A ferramenta alterou o código, mas todos os seus testes continuaram passando. Isso é péssimo! Revela uma brecha na sua suíte de testes (cobertura cosmética).
- Timeout / Error: A mutação gerou um loop infinito ou erro de compilação.
- Mutation Score (Pontuação de Mutação): O percentual real de mutantes exterminados em relação ao total gerado. É o verdadeiro indicador de qualidade da suíte de testes.
A Ilusão do Code Coverage vs. Mutation Score 💡
A cobertura de código tradicional (Code Coverage) mede apenas se a linha foi executada pelo interpretador durante a suíte de testes. Ela NÃO garante que você fez as asserções (expect) corretas para pegar regressões.
Olha só este exemplo clássico de "cobertura cosmética":
// Código da aplicação
export function calcularDesconto(valorTotal: number): number {
if (valorTotal >= 100) {
return valorTotal * 0.1; // 10% de desconto
}
return 0;
}
E veja este teste ingênuo:
describe('calcularDesconto', () => {
it('deve executar o cálculo de desconto', () => {
// Executa a função passando 150
calcularDesconto(150);
// Reparou que NÃO EXISTE expect() validando o retorno???
});
});
Se você rodar o relatório do Jest, ele dirá: 100% COVERAGE!
Mas se alguém alterar o código para retornar valorTotal * 0.5 (50% de desconto), o teste continuará passando! O relatório de cobertura mentiu para você.
O Stryker entra exatamente para destruir essa ilusão: ele troca valorTotal * 0.1 por valorTotal / 0.1, roda o teste, descobre que o teste continuou passando e grita: MUTANTE SOBREVIVEU! 🛑.
Quando USAR (e quando NÃO usar) Testes de Mutação? 🎯
✅ Quando USAR:
- Regras de Negócio Críticas: Cálculos financeiros, motores de crédito, regras fiscais, algoritmos de desconto e permissões de segurança.
- Bibliotecas Core e Pacotes Reutilizáveis: Códigos compartilhados por toda a empresa onde um bug impacta dezenas de microsserviços.
- Validar a Efetividade da Suíte de Testes: Quando o time atinge 80% ou 90% de cobertura tradicional e quer garantir que essa cobertura é real e não apenas cosmética.
🛑 Quando NÃO usar:
- Em cada
git commitlocal sem filtro: Como o Stryker roda a suíte de testes dezenas de vezes (uma para cada mutante), ele pode ser lento se executado em projetos inteiros de uma só vez. - Arquivos de Configuração ou DTOs sem Lógica: Interfaces TypeScript, arquivos de rotas puras ou schemas sem regras condicionais.
Ponto fundamental: Stryker vs Jest (Quem faz o quê?) 🤝
Antes de colocar a mão no código, vale esclarecer a relação entre as ferramentas:
O Stryker NÃO substitui o Jest. Ele precisa do Jest para funcionar!
- O Jest (Test Runner): Executa os arquivos de teste e verifica as asserções (
expect). - O Stryker (Mutation Framework): Cria mutantes do seu código na memória, chama o Jest para rodar a cada mutação e compila o relatório de sobrevivência dos mutantes.
Como o Stryker funciona por baixo dos panos? 🔍
Abaixo está o fluxo completo de 5 etapas realizado pelo Stryker durante a execução:
┌─────────────────┐ ┌────────────────────┐ ┌──────────────────────┐
│ 1. Análise do │ ────► │ 2. Injeção de │ ────► │ 3. Execução do │
│ Código-Fonte │ │ Mutantes no Código│ │ Jest p/ Cada Mutante│
└─────────────────┘ └────────────────────┘ └──────────┬───────────┘
│
┌─────────────────┐ ┌────────────────────┐ │
│ 5. Relatório │ ◄──── │ 4. Cálculo do │ ◄────────────────┘
│ HTML Final │ │ Mutation Score │
└─────────────────┘ └────────────────────┘
- Análise de AST (Abstract Syntax Tree): O Stryker lê o seu código TypeScript/JavaScript e identifica pontos mutáveis (operadores lógicos, numéricos, strings e condicionais).
- Criação dos Mutantes: Ele gera cópias em memória aplicando alterações lógicas (ex: alterando
>para>=). - Execução Direcionada: Para cada mutante, ele roda apenas os testes cobrindo aquela linha específica (otimização de performance).
- Verificação de Status: Se o Jest falhar, o mutante é marcado como Killed 🟢. Se o Jest passar, o mutante é marcado como Survived 🛑.
- Relatório Visual: Gera um painel interativo em HTML destacando em vermelho as linhas exatas onde faltam asserções.
PARTE PRÁTICA: Passo a Passo do Zero (Baby Steps) 🛠️
Vamos construir juntos um exemplo do mundo real do zero.
Passo 1: Instalação das Dependências no Node.js
No terminal do seu projeto Node.js / TypeScript, instale o Stryker e o executador do Jest:
npm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner
Passo 2: Configurando o Stryker (stryker.config.mjs)
Crie o arquivo de configuração stryker.config.mjs na raiz do seu projeto:
// @ts-check
/** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */
const config = {
packageManager: 'npm',
reporters: ['html', 'clear-text', 'progress'],
testRunner: 'jest',
coverageAnalysis: 'perTest',
mutate: [
'src/services/**/*.ts',
'!src/services/**/*.spec.ts' // Ignora os arquivos de teste na mutação
],
thresholds: {
high: 80,
low: 60,
break: 70 // Se o Mutation Score for menor que 70%, o Stryker falha com erro!
}
};
export default config;
Passo 3: Configurando os Scripts no package.json
Adicione os scripts no package.json:
{
"scripts": {
"test": "jest",
"test:mutation": "stryker run"
}
}
Passo 4: O Código de Negócio (src/services/discountService.ts)
Vamos criar um serviço de cálculo de frete e desconto com regras de limite:
export interface Order {
value: number;
isVipCustomer: boolean;
}
export function calculateShipping(order: Order): number {
// Clientes VIP têm frete grátis em pedidos a partir de R$ 100
if (order.isVipCustomer && order.value >= 100) {
return 0;
}
// Pedidos comuns acima de R$ 200 ganham frete grátis
if (order.value > 200) {
return 0;
}
// Frete padrão de R$ 20
return 20;
}
Passo 5: O Teste Incompleto (src/services/discountService.spec.ts)
Agora criamos um teste que atinge 100% de Cobertura Tradicional, mas possui furos graves:
import { calculateShipping } from './discountService';
describe('calculateShipping', () => {
it('deve calcular o frete para cliente VIP e cliente comum', () => {
// Teste 1: VIP com valor 150 -> frete 0
expect(calculateShipping({ value: 150, isVipCustomer: true })).toBe(0);
// Teste 2: Comum com valor 250 -> frete 0
expect(calculateShipping({ value: 250, isVipCustomer: false })).toBe(0);
// Teste 3: Comum com valor 50 -> frete 20
expect(calculateShipping({ value: 50, isVipCustomer: false })).toBe(20);
});
});
Se você rodar npm test -- --coverage, o Jest reportará: 100% de cobertura de código! 🎉
Passo 6: Executando o Stryker e Analisando a Sobrevivência dos Mutantes
No terminal, execute:
npm run test:mutation
Saída do Terminal do Stryker 🛑:
[INFO] MutationTestExecutor - Initial test run succeeded. Testing 6 mutants.
[INFO] ProgressReporter - Tested mutant 1/6: Survived (EqualityOperator: >= -> >)
[INFO] ProgressReporter - Tested mutant 2/6: Killed (BooleanLiteral: true -> false)
[INFO] ProgressReporter - Tested mutant 3/6: Survived (EqualityOperator: > -> >=)
[INFO] ProgressReporter - Tested mutant 4/6: Killed (ConditionalExpression: true -> false)
All tests passed for 2 mutants!
Mutation score: 66.67% (4 killed, 2 survived)
😱 MUTATION SCORE: 66.67%! O Stryker pegou 2 mutantes sobreviventes!
Por que os mutantes sobreviveram?
- Mutante 1 (
>=para>): O Stryker alterouorder.value >= 100paraorder.value > 100. Como nosso teste só passou o valor150para o cliente VIP, ele nunca testou o valor limite exato de100! - Mutante 3 (
>para>=): O Stryker alterouorder.value > 200paraorder.value >= 200. Como nosso teste só passou o valor250, nunca testamos o valor exato de200!
Passo 7: Matando os Mutantes! 🗡️🟢
Para elevar nossa pontuação para 100% e garantir proteção real, adicionamos os casos de teste nos valores de borda em src/services/discountService.spec.ts:
import { calculateShipping } from './discountService';
describe('calculateShipping', () => {
it('deve dar frete grátis para cliente VIP no valor exato de R$ 100 (limite)', () => {
expect(calculateShipping({ value: 100, isVipCustomer: true })).toBe(0);
expect(calculateShipping({ value: 99.99, isVipCustomer: true })).toBe(20);
});
it('deve aplicar frete grátis para pedido comum apenas ACIMA de R$ 200', () => {
expect(calculateShipping({ value: 200, isVipCustomer: false })).toBe(20); // No valor 200 paga frete
expect(calculateShipping({ value: 200.01, isVipCustomer: false })).toBe(0); // Acima de 200 frete grátis
});
it('deve aplicar frete padrão para valores baixos', () => {
expect(calculateShipping({ value: 50, isVipCustomer: false })).toBe(20);
});
});
Rode novamente no terminal:
npm run test:mutation
Nova Saída de Sucesso 🟢:
All tests failed for all 6 mutants!
Mutation score: 100.00% (6 killed, 0 survived)
🎉 100% MUTATION SCORE! Todos os mutantes foram mortos com sucesso!
Como Rodar o Stryker no CI/CD (GitHub Actions) sem Travar a Esteira 🚀
Para não desacelerar os PRs, configure o GitHub Actions para rodar o Stryker apenas nos arquivos modificados na branch:
name: Mutation Testing CI
on:
pull_request:
branches: [main, master]
jobs:
stryker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Necessário para comparar arquivos alterados
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm test
# Executa a mutação apenas nos arquivos alterados no PR
- name: Run Stryker on Changed Files
run: npx stryker run --mutate "$(git diff --name-only origin/main...HEAD | grep 'src/' | tr '\n' ',')"
Resumo dos Benefícios 🎯
- Fim da cobertura maquiada: Descubra se os testes realmente testam o comportamento ou só passam pelas linhas.
- Proteção contra regressão em valores de borda: Garante que alterações condicionais (
>,>=,<,<=) não passem despercebidas. - Confiança máxima no refactor: Você pode mexer na lógica sabendo que qualquer desalinhamento fará um teste falhar de verdade.
O que vem por aí na série "Testes para Dev"? 🪝
Seus testes cobrem unidades, integrações, CI, contratos e agora possuem mutação à prova de falhas. Mas me responde: e quando o usuário real entra na aplicação, clica nos botões, preenche formulários e navega pelas telas do navegador?
No Capítulo #6, vamos falar sobre Testes End-to-End (E2E) com Cypress! Vamos aprender a simular jornadas completas de navegação do usuário do zero!
Deixe seu comentário e até o próximo post! 🚀