<h2>Entendendo Mutation Testing: Além da Cobertura de Código</h2>
<p>Quando começamos a trabalhar com testes automatizados, frequentemente nos focamos em uma métrica simples: cobertura de código. Uma cobertura de 80%, 90% ou até 100% nos passa a sensação de segurança, mas será que realmente estamos testando bem? A resposta é não. Cobertura de código apenas mede quantas linhas seu código executa durante os testes, não se seus testes realmente validam o comportamento esperado.</p>
<p>Mutation testing é uma técnica que revoluciona essa perspectiva. A ideia é simples, mas poderosa: o framework introduz pequenas alterações (mutações) no seu código-fonte e executa todos os seus testes. Se um teste falhar com a mutação, ele "matou" a mutação. Se todos os testes passarem mesmo com o código alterado, significa que seus testes não estão validando aquele comportamento adequadamente. Essa abordagem nos força a escrever testes de verdade, não apenas testes que tocam o código.</p>
<h2>Instalação e Configuração do Stryker em um Projeto TypeScript</h2>
<p>O Stryker é um mutation testing framework moderno, otimizado e fácil de usar. Vamos começar instalando e configurando em um projeto TypeScript do zero.</p>
<h3>Preparação do Ambiente</h3>
<p>Primeiro, crie um diretório do projeto e inicialize o Node.js:</p>
<pre><code class="language-bash">mkdir mutation-testing-demo
cd mutation-testing-demo
npm init -y
npm install --save-dev typescript ts-node @types/node
npm install --save-dev @stryker-mutator/core @stryker-mutator/typescript-checker</code></pre>
<p>Crie um arquivo <code>tsconfig.json</code>:</p>
<pre><code class="language-json">{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/*/"],
"exclude": ["node_modules", "dist"]
}</code></pre>
<p>Agora instale um framework de testes. Vamos usar Jest:</p>
<pre><code class="language-bash">npm install --save-dev jest ts-jest @types/jest</code></pre>
<p>Configure o Jest criando <code>jest.config.js</code>:</p>
<pre><code class="language-javascript">module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['/__tests__//.test.ts', '/?(.)+(spec|test).ts'],
rootDir: 'src'
};</code></pre>
<h3>Configuração do Stryker</h3>
<p>Execute o configurador interativo:</p>
<pre><code class="language-bash">npx stryker init</code></pre>
<p>Este comando fará perguntas sobre seu setup. Para nosso caso, configure para usar TypeScript e Jest. Ele criará <code>stryker.conf.json</code>. Você pode ajustar manualmente:</p>
<pre><code class="language-json">{
"mutate": ["src/*/.ts", "!src/*/.test.ts"],
"testRunner": "jest",
"checkers": ["typescript"],
"tsconfigFile": "tsconfig.json",
"reporters": ["html", "clear-text", "progress"],
"timeoutMS": 5000,
"concurrency": 4
}</code></pre>
<h2>Escrevendo Testes Fracos vs. Testes Fortes com Exemplos Práticos</h2>
<p>A diferença entre um teste fraco e um teste forte só fica evidente quando usamos mutation testing. Vamos demonstrar com um exemplo real.</p>
<h3>Exemplo 1: Um Teste Fraco que Não Detecta Mutações</h3>
<p>Crie <code>src/calculator.ts</code>:</p>
<pre><code class="language-typescript">export class Calculator {
add(a: number, b: number): number {
return a + b;
}
isPositive(value: number): boolean {
if (value > 0) {
return true;
}
return false;
}
divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
}</code></pre>
<p>Agora crie <code>src/calculator.weak.test.ts</code> — um teste fraco:</p>
<pre><code class="language-typescript">import { Calculator } from './calculator';
describe('Calculator - Testes Fracos', () => {
let calc: Calculator;
beforeEach(() => {
calc = new Calculator();
});
it('should add two numbers', () => {
const result = calc.add(2, 3);
expect(result).toBeTruthy(); // Teste fraco: apenas verifica se é truthy
});
it('should check if positive', () => {
calc.isPositive(5);
// Teste fraco: não verifica o resultado!
});
it('should divide numbers', () => {
const result = calc.divide(10, 2);
expect(result).toBeDefined(); // Teste fraco: apenas verifica se existe
});
});</code></pre>
<p>Estes testes passarão em qualquer circunstância. Se você mutar <code>a + b</code> para <code>a - b</code>, o teste de adição ainda passará porque o resultado será truthy. O Stryker conseguirá sobreviver com essas mutações.</p>
<h3>Exemplo 2: Testes Fortes que Matam Mutações</h3>
<p>Crie <code>src/calculator.strong.test.ts</code> — testes realmente bons:</p>
<pre><code class="language-typescript">import { Calculator } from './calculator';
describe('Calculator - Testes Fortes', () => {
let calc: Calculator;
beforeEach(() => {
calc = new Calculator();
});
it('should add two positive numbers correctly', () => {
expect(calc.add(2, 3)).toBe(5);
expect(calc.add(10, 20)).toBe(30);
expect(calc.add(-5, 5)).toBe(0);
});
it('should return true only for positive numbers', () => {
expect(calc.isPositive(5)).toBe(true);
expect(calc.isPositive(0.1)).toBe(true);
expect(calc.isPositive(0)).toBe(false);
expect(calc.isPositive(-1)).toBe(false);
});
it('should divide numbers correctly', () => {
expect(calc.divide(10, 2)).toBe(5);
expect(calc.divide(15, 3)).toBe(5);
});
it('should throw error when dividing by zero', () => {
expect(() => calc.divide(10, 0)).toThrow('Division by zero');
});
it('should handle edge cases in division', () => {
expect(calc.divide(0, 5)).toBe(0);
expect(calc.divide(-10, 2)).toBe(-5);
});
});</code></pre>
<p>A diferença é clara: verificamos valores específicos, casos extremos e comportamentos esperados. Se o Stryker mutar <code>></code> para <code>>=</code> em <code>isPositive</code>, o teste falhará porque esperamos que <code>isPositive(0)</code> retorne <code>false</code>. Se mutar <code>+</code> para <code>-</code>, o teste de adição falhará imediatamente.</p>
<h2>Executando Mutation Testing e Interpretando Resultados</h2>
<h3>Rodando o Stryker</h3>
<p>Com a configuração pronta e testes escritos, execute:</p>
<pre><code class="language-bash">npx stryker run</code></pre>
<p>O Stryker analisará seu código, criará múltiplas versões mutantes, executará todos os testes contra cada mutante e gerará relatórios. Isso pode levar tempo dependendo do tamanho do projeto.</p>
<h3>Entendendo o Relatório</h3>
<p>O Stryker gera um relatório HTML em <code>reports/mutation/html/index.html</code>. O relatório mostra:</p>
<ul>
<li><strong>Killed</strong>: Mutações que foram detectadas por seus testes (bom!)</li>
<li><strong>Survived</strong>: Mutações que não foram detectadas (problema!)</li>
<li><strong>Timeout</strong>: Mutações que causaram loop infinito</li>
<li><strong>Compile Error</strong>: Mutações que não compilam</li>
</ul>
<p>A métrica principal é o <strong>Mutation Score</strong>, calculado como: <code>(Killed / (Killed + Survived)) × 100</code>. Um score de 100% significa que seus testes detectam todas as mutações. Na prática, 80% é excelente.</p>
<p>Você também pode usar a saída no terminal:</p>
<pre><code class="language-bash">npx stryker run --reporters clear-text</code></pre>
<p>Isso mostra um resumo direto no console, útil para CI/CD.</p>
<h2>Estratégias Práticas para Melhorar Mutation Score</h2>
<h3>Identificando Pontos Cegos</h3>
<p>Quando o Stryker mostra mutações que sobrevivem, você encontrou pontos fracos em seus testes. Vamos criar um exemplo mais realista:</p>
<pre><code class="language-typescript">// src/userService.ts
export class UserService {
private users: Map<number, { id: number; name: string; active: boolean }> = new Map();
private nextId = 1;
createUser(name: string): number {
if (!name || name.trim() === '') {
throw new Error('Name cannot be empty');
}
const id = this.nextId++;
this.users.set(id, { id, name: name.trim(), active: true });
return id;
}
getUserById(id: number) {
const user = this.users.get(id);
if (!user) {
return null;
}
return user;
}
deactivateUser(id: number): boolean {
const user = this.users.get(id);
if (!user) {
return false;
}
user.active = false;
return true;
}
getActiveUsers() {
return Array.from(this.users.values()).filter(u => u.active);
}
}</code></pre>
<p>Agora, um teste que parece bom, mas deixa mutações passar:</p>
<pre><code class="language-typescript">// src/userService.weak.test.ts
import { UserService } from './userService';
describe('UserService - Testes Incompletos', () => {
let service: UserService;
beforeEach(() => {
service = new UserService();
});
it('should create a user', () => {
const id = service.createUser('João');
expect(id).toBe(1); // Apenas valida o ID retornado
});
it('should get user by id', () => {
service.createUser('Maria');
const user = service.getUserById(1);
expect(user).not.toBeNull(); // Valida apenas existência
});
it('should deactivate user', () => {
service.createUser('Pedro');
service.deactivateUser(1);
expect(true).toBe(true); // Teste completamente vazio!
});
});</code></pre>
<p>Testes forte que matam mutações:</p>
<pre><code class="language-typescript">// src/userService.strong.test.ts
import { UserService } from './userService';
describe('UserService - Testes Abrangentes', () => {
let service: UserService;
beforeEach(() => {
service = new UserService();
});
describe('createUser', () => {
it('should create user with unique incremental IDs', () => {
const id1 = service.createUser('Alice');
const id2 = service.createUser('Bob');
expect(id1).toBe(1);
expect(id2).toBe(2);
});
it('should trim user name before storing', () => {
service.createUser(' Charlie ');
const user = service.getUserById(1);
expect(user?.name).toBe('Charlie');
});
it('should throw error for empty names', () => {
expect(() => service.createUser('')).toThrow('Name cannot be empty');
expect(() => service.createUser(' ')).toThrow('Name cannot be empty');
});
it('should mark new users as active', () => {
service.createUser('David');
const user = service.getUserById(1);
expect(user?.active).toBe(true);
});
});
describe('getUserById', () => {
it('should return null for non-existent user', () => {
expect(service.getUserById(999)).toBeNull();
});
it('should return complete user object', () => {
service.createUser('Eve');
const user = service.getUserById(1);
expect(user).toEqual({
id: 1,
name: 'Eve',
active: true
});
});
});
describe('deactivateUser', () => {
it('should deactivate an existing user', () => {
service.createUser('Frank');
const result = service.deactivateUser(1);
expect(result).toBe(true);
expect(service.getUserById(1)?.active).toBe(false);
});
it('should return false for non-existent user', () => {
const result = service.deactivateUser(999);
expect(result).toBe(false);
});
it('should not affect other users', () => {
service.createUser('Grace');
service.createUser('Henry');
service.deactivateUser(1);
expect(service.getUserById(2)?.active).toBe(true);
});
});
describe('getActiveUsers', () => {
it('should return only active users', () => {
service.createUser('Iris');
service.createUser('Jack');
service.createUser('Karen');
service.deactivateUser(2);
const activeUsers = service.getActiveUsers();
expect(activeUsers).toHaveLength(2);
expect(activeUsers.map(u => u.id)).toEqual([1, 3]);
});
it('should return empty array when no users exist', () => {
expect(service.getActiveUsers()).toEqual([]);
});
it('should return empty array when all users are inactive', () => {
service.createUser('Liam');
service.deactivateUser(1);
expect(service.getActiveUsers()).toEqual([]);
});
});
});</code></pre>
<p>A diferença é gritante. Os testes fortes:</p>
<ul>
<li>Validam valores específicos, não apenas existência</li>
<li>Testam casos extremos (strings vazias, valores null, etc.)</li>
<li>Verificam múltiplos cenários no mesmo teste quando apropriado</li>
<li>Garantem que o estado muda corretamente</li>
</ul>
<h3>Checklist para Melhorar Mutation Score</h3>
<p>Quando encontrar mutações que sobrevivem, pergunte-se:</p>
<ul>
<li><strong>Estou validando o resultado exato?</strong> Use <code>toBe()</code> ao invés de <code>toBeTruthy()</code></li>
<li><strong>Estou testando todas as branches?</strong> Use ferramentas de cobertura para identificar caminhos não testados</li>
<li><strong>Estou testando casos extremos?</strong> Zero, valores negativos, strings vazias, null</li>
<li><strong>Estou validando mudanças de estado?</strong> Não apenas retornos, mas também como o objeto muda</li>
<li><strong>Estou testando as condições corretamente?</strong> <code>></code>, <code>>=</code>, <code><</code>, <code><=</code>, <code>===</code> são diferentes</li>
</ul>
<h2>Conclusão</h2>
<p>Você aprendeu que <strong>mutation testing não é uma métrica de cobertura melhorada, é uma mudança de filosofia</strong>: testes reais validam comportamento, não apenas linha de código. O Stryker em TypeScript fornece uma ferramenta acessível para implementar essa mentalidade no seu workflow diário.</p>
<p>Em segundo lugar, <strong>testes fracos e testes fortes têm uma diferença fundamental</strong>: testes fracos apenas "tocam" o código, enquanto testes fortes validam o comportamento esperado com precisão. Usar mutation testing força você a escrever o segundo tipo, aumentando a confiabilidade do seu software significativamente.</p>
<p>Por fim, <strong>melhorar mutation score é um processo iterativo</strong>: execute o Stryker regularmente, analise mutações que sobrevivem, e reescreva testes para cobrir esses cenários. A combinação de cobertura de código tradicional com mutation score cria uma rede de segurança praticamente impenetrável contra bugs.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://stryker-mutator.io/" target="_blank" rel="noopener noreferrer">Stryker Mutator - Documentação Oficial</a></li>
<li><a href="https://github.com/testing-library/testing-library-docs" target="_blank" rel="noopener noreferrer">Testing Library TypeScript Best Practices</a></li>
<li><a href="https://pitest.org/" target="_blank" rel="noopener noreferrer">Mutation Testing: Evaluating Test Quality</a> — Referência de implementação similar em Java</li>
<li><a href="https://jestjs.io/docs/getting-started" target="_blank" rel="noopener noreferrer">Jest Documentation - Complete Testing Guide</a></li>
<li><a href="https://www.amazon.com/Art-Software-Testing-Glenford-Myers/dp/1118031962" target="_blank" rel="noopener noreferrer">The Art of Software Testing - Glenford J. Myers</a> — Clássico sobre qualidade de testes</li>
</ul>
<p><!-- FIM --></p>