Ga naar inhoud

Mutatietesten uitgelegd

Spec: ISO/IEC/IEEE 29119-4 Spec: PHPUnit Evidence: Test-backed

Regeldekking laat zien dat een regel tijdens de testsuite is uitgevoerd. Ze laat niet zien of een test gefaald zou zijn als die regel fout was. Mutatietesten overbrugt die kloof door de code opzettelijk kapot te maken en te controleren of de tests dat opmerken. Deze pagina legt uit wat een mutatiescore betekent en hoe NextPDF die als diagnosemiddel gebruikt, niet als trofee.

Dekking is een van de vertrouwdste testmetrieken en tegelijk een van de meest misleidende. Een test die een methode aanroept en niets controleert, voert elke regel ervan uit: perfecte dekking, geen enkele detectie. De normliteratuur maakt duidelijk dat de ordening tussen dekkingscriteria geen indicatie geeft van hun vermogen om fouten bloot te leggen. Dat vermogen noemt zij testeffectiviteit (ISO/IEC/IEEE 29119-4, §C.2.4). Een dekkingspercentage en een garantie op het vinden van fouten zijn verschillende beweringen.

Voor een PDF-engine is dit geen theorie. Een controle van de byte-range van een handtekening, een cross-reference-offset, een coderingsvertakking — tests kunnen dit allemaal volledig “dekken” zonder ooit de waarde te controleren waar het om draait. Een groene suite die op zwakke tests rust, is erger dan een eerlijke leemte, omdat zij iedereen er actief van weerhoudt om nog te kijken.

  • Mutatietesten brengt duizenden kleine, opzettelijke wijzigingen (mutanten) aan in de broncode — een < wijzigen in <=, een + in -, een return-waarde — en voert telkens de tests opnieuw uit.
  • Als een test faalt op een mutant, wordt de mutant gedood: een test heeft dat gedrag daadwerkelijk gecontroleerd. Als elke test toch slaagt, is de mutant ontsnapt: het gedrag is uitgevoerd maar nooit gecontroleerd.
  • De Mutation Score Indicator (MSI) is, grofweg, het aantal gedode mutanten gedeeld door het totaal aan niet-equivalente mutanten. Hij meet of tests wijzigingen detecteren, niet of zij de code uitvoeren.
  • Sommige mutanten zijn equivalent — zij kunnen het waarneembare gedrag niet veranderen, dus geen enkele test kan ze doden. Zulke mutanten als mislukkingen meetellen is oneerlijk. NextPDF bewijst ze en registreert ze in een grootboek in plaats van ze informeel terzijde te schuiven.
  • NextPDF gebruikt MSI om zwakke tests op te sporen en te versterken. Het is een diagnostische poort in continuous integration, geen marketinggetal.

In de engine voert NextPDF mutatietesten uit met de Infection-mutator. Die is geconfigureerd voor de productiebroncodeboom, met de mutatorfamilies voor rekenkunde, boolean, conditionele grenzen, gelijkheid, returnwaarden en verwijdering ingeschakeld — precies de operatoren die logica blootleggen die wel wordt uitgevoerd maar niet gecontroleerd. De flow verloopt mechanisch:

  1. Start green The suite must pass before mutation begins.
  2. Mutate Apply one small, deliberate change to the source.
  3. Re-run Run the tests that cover the mutated line.
  4. Killed A test failed — the behaviour is genuinely asserted.
  5. Escaped All tests still pass — a weak spot to strengthen.
  6. Equivalent No test can kill it because behaviour is unchanged — proven and ledgered, not scored as a miss.
De mutatietestlus die NextPDF uitvoert: begin met groene tests, genereer een mutant, voer de dekkende tests opnieuw uit en classificeer de mutant als gedood (een test ving hem op), ontsnapt (een leemte met wel dekking maar geen controle om te verhelpen) of bewezen-equivalent (geen enkele test kan hem doden; in een grootboek geregistreerd, niet tegen de score meegeteld).

Twee ontwerpkeuzes maken het getal betrouwbaar. Ten eerste is de score gekoppeld aan een poort. Continuous integration dwingt een minimale MSI af (en een minimale covered-MSI) en draait een diff-scoped variant op gewijzigde regels. Daardoor wordt een wijziging die wel code maar geen echte controles toevoegt, tijdens review opgemerkt en niet pas later ontdekt. Ten tweede schrijft NextPDF lastige mutanten niet stilzwijgend af. Mutanten die werkelijk semantisch equivalent zijn — bijvoorbeeld !== versus != wanneer strikte typering garandeert dat beide operanden hetzelfde type hebben — worden in een mutatiegrootboek vastgelegd met een expliciete equivalentiebewijstest. Daardoor weerspiegelt het aantal ontsnapte mutanten echte leemten in de controles, geen boekhoudkundig effect. PHPStan Level 10, strict_types en getypeerde eigenschappen maken die equivalentiebewijzen sluitend.

Evidence: Test-backed In de engine is mutatietesten geconfigureerd voor de productiebroncodemappen met de gedragsonthullende mutatorfamilies ingeschakeld. Het wordt afgedwongen via een continuous-integration-poort met een minimale MSI en een diff-scoped variant. Het is een buildcontrole, geen bijzaak.

Evidence: Test-backed Het probleem van equivalente mutanten wordt zorgvuldig afgehandeld. Semantisch equivalente mutanten worden geclassificeerd en onderbouwd met specifieke equivalentiebewijstests in een mutatiegrootboek, waarbij de geldigheid van elk bewijs steunt op PHPStan Level 10 plus strikte typering. Het aantal ontsnapte mutanten staat daarom voor echt onopgemerkt gedrag, niet voor niet te doden ruis die de score kunstmatig slechter doet lijken.

Evidence: Standard-backed Mutatietesten is een erkende techniek, geen uitvinding van NextPDF. Spec: ISO/IEC/IEEE 29119-4, §B.2.4 beschrijft het toepassen van generieke mutaties op elementen van een specificatie om specifieke mutaties voor testen af te leiden. De techniek is überhaupt nodig omdat dezelfde norm stelt dat de subsumes-ordening van dekkingscriteria ze niet rangschikt naar foutonthullend vermogen (ISO/IEC/IEEE 29119-4, §C.2.4).

Evidence: Standard-backed Dekking zelf is helder gedefinieerd, maar beperkt. Spec: PHPUnit onderscheidt regeldekking, vertakkingsdekking en paddekking. Regeldekking registreert alleen dat een uitvoerbare regel is uitgevoerd. Juist die definitie maakt duidelijk waarom zij ontoereikend is.

Het draait niet om het commando — het gaat om wat een ontsnapte mutant u vertelt:

<?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 gate

In die ontsnapte mutant zit de hele waarde. Hij wijst een echte, specifieke ontbrekende controle aan op een plek die een dekkingsrapport als volledig getest beoordeelde.

Het voornaamste misverstand is dat de mutatiescore een cijfer is dat moet worden gemaximaliseerd. Een zeer hoge MSI die wordt bereikt door tests te schrijven om mutanten te doden, is even hol als hoge dekking die wordt bereikt door methoden aan te roepen zonder te controleren. Dan is de metriek gemanipuleerd en meet zij geen detectie meer. NextPDF gebruikt MSI om zwakke tests te vinden. De opbrengst is een betere controle; pronken is uitdrukkelijk niet het doel.

Het tweede misverstand is dat elke overlevende mutant een tekortkoming in de tests is. Sommige mutanten zijn werkelijk equivalent en kunnen door geen enkele test worden gedood, omdat zij het waarneembare gedrag niet veranderen. Zulke mutanten als mislukkingen behandelen levert een oneerlijke, kunstmatig lage score op en leert mensen het rapport te negeren. NextPDF’s antwoord is om equivalentie expliciet te bewijzen en in een grootboek vast te leggen, niet om zulke mutanten stilzwijgend te onderdrukken of te doen alsof het getal slechter is dan het is.

Mutatietesten meet of tests geïnjecteerde wijzigingen detecteren. Het bewijst niet dat de code correct is. Het meet geen prestaties of conformiteit. Het kan een werkelijk equivalente mutant niet doden. De huidige mutatiescore, de geldende minimum-MSI-drempel, het aantal in het grootboek geregistreerde equivalenten en elk dekkingscijfer zijn actuele kwaliteitssignalen die uit continuous-integration-artefacten worden gegenereerd en met de build worden gepubliceerd. Ze ontbreken hier opzettelijk, omdat een getal dat in de tekst wordt geplakt veroudert en een kleine leugen wordt. Het enige stabiele feit dat deze pagina vermeldt, is PHPStan Level 10, en dat is een configuratie-eigenschap die de equivalentiebewijzen ondersteunt, geen meting.

De keuze van mutatoren, drempels en het grootboekbeleid vallen onder de mutatieconfiguratie van de engine en kunnen evolueren. Die configuratie is de autoriteit als die ooit afwijkt van deze pagina. Hier wordt geen bewering gedaan over de testeffectiviteit van enige andere bibliotheek.

  • Mutant — één enkele, opzettelijke kleine wijziging in de broncode, gebruikt om te testen of de testsuite die wijziging zou opmerken.
  • Gedode mutant — een mutant waarop ten minste één test faalde; het gedrag werd daadwerkelijk gecontroleerd.
  • Ontsnapte mutant — een mutant waarbij elke test bleef slagen. Het gedrag werd uitgevoerd maar nooit gecontroleerd — een zwakke plek om te verhelpen.
  • Equivalente mutant — een mutant die het waarneembare gedrag niet kan veranderen, dus geen enkele test kan hem doden. NextPDF bewijst zulke mutanten en legt ze in een grootboek vast.
  • MSI (Mutation Score Indicator) — grofweg het aantal gedode mutanten gedeeld door het totaal aan niet-equivalente mutanten; een maat voor detectie, niet voor uitvoering.
  • Regeldekking — een metriek die alleen registreert dat een uitvoerbare regel tijdens de suite is uitgevoerd; gedefinieerd door PHPUnit, en op zichzelf ontoereikend.