Teste de mutação explicado
Spec: ISO/IEC/IEEE 29119-4 ISO/IEC/IEEE 29119-4 Spec: PHPUnit PHPUnit Evidence: Test-backed
Visão geral
Seção intitulada “Visão geral”A cobertura de linha informa que uma linha foi executada durante a suíte de testes. Ela não informa se algum teste falharia caso aquela linha estivesse errada. O teste de mutação fecha essa lacuna ao quebrar o código deliberadamente e verificar se os testes percebem. Esta página explica o que significa um escore de mutação e como o NextPDF o usa como diagnóstico, não como troféu.
Por que isso importa
Seção intitulada “Por que isso importa”A cobertura é uma das métricas de teste mais confiadas e também uma das mais enganosas. Um teste que chama um método e não verifica nada executa todas as linhas dele: cobertura perfeita, detecção zero. A literatura normativa é explícita: a ordenação entre critérios de cobertura não indica a capacidade deles de expor defeitos. Essa capacidade é a propriedade que ela chama de eficácia de teste (ISO/IEC/IEEE 29119-4, §C.2.4). Um percentual de cobertura e uma garantia de detecção de defeitos são afirmações diferentes.
Para um mecanismo de PDF, isso não é teórico. Uma verificação de byte-range de assinatura, um deslocamento de referência cruzada, um ramo de codificação — os testes podem “cobrir” totalmente todos eles sem nunca verificar o valor que importa. Uma suíte verde apoiada em testes fracos é pior do que uma lacuna honesta, porque desencoraja ativamente qualquer investigação.
A versão resumida
Seção intitulada “A versão resumida”- Teste de mutação faz milhares de pequenas alterações deliberadas (mutantes) no código-fonte — troca um
<por<=, um+por-, um valor dereturn— e reexecuta os testes contra cada uma delas. - Se um teste falha diante de um mutante, o mutante é morto: algum teste de fato verificou aquele comportamento. Se todos os testes continuam passando, o mutante escapou: o comportamento foi executado, mas nunca verificado.
- O Mutation Score Indicator (MSI) é, grosso modo, a proporção de mutantes mortos sobre o total de mutantes não equivalentes. Ele mede se os testes detectam alterações, não se executam o código.
- Alguns mutantes são equivalentes — não conseguem alterar o comportamento observável, então nenhum teste consegue matá-los. Contá-los como falhas é desonesto. O NextPDF os prova e registra em um livro-razão, em vez de descartá-los informalmente.
- O NextPDF usa o MSI para encontrar e fortalecer testes fracos. É um portão de diagnóstico na integração contínua, não um número de marketing.
Como o NextPDF aborda isso
Seção intitulada “Como o NextPDF aborda isso”A mutação é executada no mecanismo com o mutador Infection. Ele é configurado sobre a árvore de código-fonte de produção, com as famílias de mutadores aritmética, booleana, de limite condicional, de igualdade, de valor de retorno e de remoção habilitadas — exatamente os operadores que expõem a lógica “executada, mas não verificada”. O fluxo é mecânico:
- Start green The suite must pass before mutation begins.
- Mutate Apply one small, deliberate change to the source.
- Re-run Run the tests that cover the mutated line.
- Killed A test failed — the behaviour is genuinely asserted.
- Escaped All tests still pass — a weak spot to strengthen.
- Equivalent No test can kill it because behaviour is unchanged — proven and ledgered, not scored as a miss.
Duas decisões de projeto tornam o número confiável. Primeiro, o escore é conectado como um portão. A integração contínua impõe um MSI mínimo (e um covered-MSI mínimo) e executa uma variante restrita ao diff sobre as linhas alteradas. Com isso, uma alteração que adiciona código, mas não verificações reais, é pega na revisão, não descoberta depois. Segundo, o NextPDF não descarta silenciosamente mutantes inconvenientes. Mutantes que são genuinamente semanticamente equivalentes — por exemplo, !== versus != quando a tipagem estrita garante que ambos os operandos compartilham um tipo — são registrados em um livro-razão de mutação com um teste explícito de prova de equivalência. Assim, a contagem de escapados reflete lacunas reais, não escrituração contábil. PHPStan Level 10 mais strict_types mais propriedades tipadas é o que torna sólidas essas provas de equivalência.
O que as evidências dizem
Seção intitulada “O que as evidências dizem”Evidence: Test-backed O teste de mutação é configurado no mecanismo, nos diretórios de código-fonte de produção, com as famílias de mutadores que revelam comportamento habilitadas. Ele é imposto como um portão de integração contínua com um MSI mínimo e uma variante restrita ao diff. É uma verificação de build, não uma consideração tardia.
Evidence: Test-backed O problema dos mutantes equivalentes é tratado de forma honesta. Mutantes semanticamente equivalentes são classificados e respaldados por testes dedicados de prova de equivalência em um livro-razão de mutação, com a solidez de cada prova apoiada em PHPStan Level 10 mais tipagem estrita. A contagem de escapados representa, portanto, comportamento real não detectado, não ruído impossível de matar inflado em um escore de aparência pior.
Evidence: Standard-backed A mutação é uma técnica reconhecida, não uma invenção do NextPDF. Spec: ISO/IEC/IEEE 29119-4, §B.2.4 ISO/IEC/IEEE 29119-4 §B.2.4 descreve a aplicação de mutações genéricas a elementos de uma especificação para derivar mutações específicas para teste. A técnica é necessária justamente porque a mesma norma afirma que a ordenação por subsunção dos critérios de cobertura não os ordena por capacidade de expor defeitos (ISO/IEC/IEEE 29119-4, §C.2.4).
Evidence: Standard-backed A cobertura em si é bem definida e limitada. Spec: PHPUnit PHPUnit distingue cobertura de linha, de ramo e de caminho. A cobertura de linha registra apenas que uma linha executável foi executada. Conhecer a definição é o que torna evidente sua insuficiência.
Exemplo prático
Seção intitulada “Exemplo prático”O ponto não é o comando — é o que um mutante escapado diz a você:
<?php
declare(strict_types=1);
final class ByteRange{ // Suppose the production guard is: // if ($offset < 0) { throw new InvalidByteRange(); } public function assertNonNegative(int $offset): void { if ($offset < 0) { throw new InvalidByteRange('offset must be >= 0'); } }}
// A test that EXECUTES this line but does not assert the boundary:// $byteRange->assertNonNegative(5); // no exception expected, none asserted// gives 100% line coverage of assertNonNegative().//// Mutation flips `< 0` to `<= 0`. Behaviour now differs ONLY at $offset === 0.// If no test passes 0 and asserts what happens, every test still passes:// the mutant ESCAPED. Coverage said "tested"; mutation said "the boundary// is unasserted". The fix is a test that pins offset === 0, not a higher// target.//// composer mutation:diff → mutate only changed lines, enforce min MSI// composer mutation:full → full-tree mutation gateEsse mutante escapado é toda a proposta de valor. Ele localizou uma verificação real, específica e ausente que um relatório de cobertura havia classificado como totalmente testada.
Equívoco comum
Seção intitulada “Equívoco comum”O equívoco principal é achar que o escore de mutação é uma nota a maximizar. Um MSI muito alto obtido escrevendo testes para matar mutantes é tão vazio quanto uma cobertura alta obtida chamando métodos sem verificar nada. A métrica foi manipulada e não mede mais a detecção. O NextPDF usa o MSI para encontrar testes fracos. A entrega é uma verificação melhor; exibir o número não é o objetivo.
O segundo equívoco é achar que todo mutante sobrevivente é um defeito nos testes. Alguns mutantes são genuinamente equivalentes e não podem ser mortos por nenhum teste, porque não alteram o comportamento observável. Tratá-los como falhas produz um escore desonesto e artificialmente baixo e ensina as pessoas a ignorar o relatório. A resposta do NextPDF é provar a equivalência explicitamente e registrá-la no livro-razão, não suprimi-la silenciosamente nem fingir que o número é pior do que realmente é.
Limites e fronteiras
Seção intitulada “Limites e fronteiras”O teste de mutação mede se os testes detectam alterações injetadas. Ele não prova que o código está correto. Ele não mede desempenho nem conformidade. Ele não consegue matar um mutante verdadeiramente equivalente. O escore de mutação atual, o limite de MSI mínimo em vigor, o número de equivalentes registrados no livro-razão e qualquer número de cobertura são sinais de qualidade vivos, gerados a partir de artefatos de integração contínua e publicados com o build. Eles ficam deliberadamente ausentes aqui, porque um número colado na prosa envelhece e vira uma pequena mentira. O único fato estável que esta página afirma é PHPStan Level 10, e isso é uma propriedade de configuração que sustenta as provas de equivalência, não uma medição.
A seleção de mutadores, os limites e a política do livro-razão pertencem à configuração de mutação do mecanismo e podem evoluir. Essa configuração é a autoridade caso algum dia discorde desta página. Esta página não faz nenhuma afirmação sobre a eficácia de teste de qualquer outra biblioteca.
Documentos relacionados
Seção intitulada “Documentos relacionados”- A pirâmide de testes do NextPDF — as cinco camadas cujos testes o teste de mutação audita em busca de detecção real de defeitos.
- Tipos estritos, em todo lugar — como PHPStan Level 10 e a tipagem estrita tornam sólidas as provas de mutantes equivalentes.
- Teste com arquivos golden — outra camada cujo poder de detecção o teste de mutação ajuda a validar.
Glossário
Seção intitulada “Glossário”- Mutante — uma única pequena alteração deliberada no código-fonte, usada para testar se a suíte de testes notaria essa alteração.
- Mutante morto — um mutante que fez pelo menos um teste falhar; o comportamento foi genuinamente verificado.
- Mutante escapado — um mutante que deixou todos os testes passando. O comportamento foi executado, mas nunca verificado — um ponto fraco a corrigir.
- Mutante equivalente — um mutante que não consegue alterar o comportamento observável, então nenhum teste consegue matá-lo. O NextPDF os prova e registra no livro-razão.
- MSI (Mutation Score Indicator) — grosso modo, os mutantes mortos divididos pelo total de mutantes não equivalentes; uma medida de detecção, não de execução.
- Cobertura de linha — uma métrica que registra apenas que uma linha executável foi executada durante a suíte; definida pelo PHPUnit e insuficiente por si só.