JavaScript Avançado

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

8 min de leitura

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

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

<h2>O Que é Mutation Testing e Por Que Importa</h2>

<p>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 <strong>realmente conseguem detectar bugs</strong>. 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.</p>

<p>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 <strong>Stryker</strong>, que automatiza todo esse processo com inteligência e performance.</p>

<h2>Como Funciona o Stryker em Prática</h2>

<h3>Instalação e Configuração Inicial</h3>

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

<pre><code class="language-bash">npm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner

npx stryker init</code></pre>

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

<pre><code class="language-javascript">// stryker.conf.js

export default {

testRunner: &#039;jest&#039;,

testPathPattern: &#039;./src/*/.test.js&#039;,

mutate: [&#039;src/*/.js&#039;, &#039;!src/*/.test.js&#039;, &#039;!src/*/.spec.js&#039;],

reporters: [&#039;html&#039;, &#039;clear-text&#039;, &#039;progress&#039;],

timeoutMS: 5000,

concurrency: 4

};</code></pre>

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

<h3>Executando e Interpretando Resultados</h3>

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

<ul>

<li><strong>Killed</strong>: seu teste detectou a mutação (bom!)</li>

<li><strong>Survived</strong>: a mutação não foi detectada (problema!)</li>

<li><strong>Timeout</strong>: a mutação causou loop infinito</li>

<li><strong>Ignored</strong>: você marcou para ignorar</li>

</ul>

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

<h2>Exemplo Real: Testando uma Função de Validação</h2>

<h3>Código Original com Teste Inadequado</h3>

<pre><code class="language-javascript">// src/validator.js

export function validateEmail(email) {

if (email.includes(&#039;@&#039;)) {

return true;

}

return false;

}

// src/validator.test.js (RUIM)

test(&#039;validateEmail returns true for email with @&#039;, () =&gt; {

expect(validateEmail(&#039;user@example.com&#039;)).toBe(true);

});</code></pre>

<p>Este teste tem cobertura 100% de linhas, mas é fraco. O Stryker criará mutações como <code>email.includes(&#039;@&#039;)</code> → <code>email.includes(&#039;#&#039;)</code>, e seu teste não falhará. Mutation score: baixo.</p>

<h3>Teste Adequado Que Mata Mutações</h3>

<pre><code class="language-javascript">// src/validator.test.js (BOM)

describe(&#039;validateEmail&#039;, () =&gt; {

test(&#039;returns true for valid email&#039;, () =&gt; {

expect(validateEmail(&#039;user@example.com&#039;)).toBe(true);

});

test(&#039;returns false for email without @&#039;, () =&gt; {

expect(validateEmail(&#039;userexample.com&#039;)).toBe(false);

});

test(&#039;returns false for empty string&#039;, () =&gt; {

expect(validateEmail(&#039;&#039;)).toBe(false);

});

test(&#039;returns false for @ only&#039;, () =&gt; {

expect(validateEmail(&#039;@&#039;)).toBe(false);

});

});</code></pre>

<p>Agora temos limites claros. Mutações como trocar <code>includes(&#039;@&#039;)</code> por <code>includes(&#039;#&#039;)</code>, ou <code>true</code> por <code>false</code>, serão capturadas. O mutation score sobe significativamente.</p>

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

<h2>Estratégias para Melhorar Mutation Score</h2>

<h3>Cobertura de Condições Booleanas</h3>

<p>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:</p>

<pre><code class="language-javascript">// src/permission.js

export function canEdit(user) {

return user.isAdmin || user.isOwner;

}

// Testes que cobrem todas as combinações booleanas

test(&#039;returns true if user is admin&#039;, () =&gt; {

expect(canEdit({ isAdmin: true, isOwner: false })).toBe(true);

});

test(&#039;returns true if user is owner&#039;, () =&gt; {

expect(canEdit({ isAdmin: false, isOwner: true })).toBe(true);

});

test(&#039;returns false if neither admin nor owner&#039;, () =&gt; {

expect(canEdit({ isAdmin: false, isOwner: false })).toBe(false);

});

test(&#039;returns true if both admin and owner&#039;, () =&gt; {

expect(canEdit({ isAdmin: true, isOwner: true })).toBe(true);

});</code></pre>

<p>O Stryker criará mutações trocando <code>||</code> por <code>&amp;&amp;</code>. Sem esses testes complementares, as mutações sobreviveriam.</p>

<h3>Validação de Valores Retornados</h3>

<p>Não confie apenas em tipo. Valide valores reais:</p>

<pre><code class="language-javascript">// src/calculator.js

export function add(a, b) {

return a + b;

}

// Fraco

test(&#039;add returns a number&#039;, () =&gt; {

expect(typeof add(2, 3)).toBe(&#039;number&#039;);

});

// Forte

test(&#039;add returns correct sum&#039;, () =&gt; {

expect(add(2, 3)).toBe(5);

expect(add(-1, 1)).toBe(0);

expect(add(0, 0)).toBe(0);

});</code></pre>

<p>A primeira versão não mata a mutação <code>a + b</code> → <code>a - b</code>. A segunda mata.</p>

<h3>Observando o Relatório HTML</h3>

<p>Após <code>npx stryker run</code>, abra o arquivo gerado em <code>reports/mutation/index.html</code>. 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.</p>

<h2>Conclusão</h2>

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

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

<h2>Referências</h2>

<ul>

<li><a href="https://stryker-mutator.io/docs/" target="_blank" rel="noopener noreferrer">Stryker - Official Documentation</a></li>

<li><a href="https://stryker-mutator.io/blog/2021-09-01/why-mutation-testing" target="_blank" rel="noopener noreferrer">Mutation Testing: A Literature Review</a></li>

<li><a href="https://jestjs.io/docs/getting-started" target="_blank" rel="noopener noreferrer">Jest Documentation - Testing Framework</a></li>

<li><a href="https://martinfowler.com/bliki/TestCoverage.html" target="_blank" rel="noopener noreferrer">Martin Fowler - Test Coverage</a></li>

<li><a href="https://learning.oreilly.com/search/?query=javascript+testing" target="_blank" rel="noopener noreferrer">Effective JavaScript Testing - O&#039;Reilly</a></li>

</ul>

Comentários

Mais em JavaScript Avançado

Boas Práticas de PostgreSQL Avançado com Node.js: Transactions, CTEs e Window Functions para Times Ágeis
Boas Práticas de PostgreSQL Avançado com Node.js: Transactions, CTEs e Window Functions para Times Ágeis

Transactions com PostgreSQL e Node.js As transações são fundamentais para gar...

Guia Completo de TypeScript para Bibliotecas: Escrevendo .d.ts e Tipos Públicos
Guia Completo de TypeScript para Bibliotecas: Escrevendo .d.ts e Tipos Públicos

Por que TypeScript é Essencial para Bibliotecas Quando você cria uma bibliote...

AbortController e Cancelamento de Operações Assíncronas: Do Básico ao Avançado
AbortController e Cancelamento de Operações Assíncronas: Do Básico ao Avançado

AbortController: Dominando o Cancelamento de Operações Assíncronas O é uma AP...