De mutatie dekking van 88% is een zeer hoge score. Het is zeldzaam voor een applicatie om in de buurt van 80% te komen, omdat het zeer hoge test standaarden vereist. We zien hier dat sommige testen nooit falen en dat er veel duplicaten zijn.
Test 2: Skip onfaalbare testen
13 testen hebben nooit gefaald in de eerste test. Ik heb deze testen uitgezet.
Het uitzetten van deze 13 testen heeft de mutatie dekking niet significant veranderd. Het verschil valt binnen de meetonzekerheid.
Deze data ondersteunt de hypothese. We hebben echter meer data nodig voordat we definitief kunnen stellen dat het verwijderen van onfaalbare testen geen impact heeft op de kwaliteit van de test suite. Het merendeel van de uitgezette testen kijken of de publieke API van Express bestaat. Stryker kan dit type bug niet genereren. Een bug in de publieke API zal afgevangen worden in andere test types die de publieke API wel actief gebruiken. De rest van de uitgezette testen dekken de functionaliteit van een dependency. Geen van de uitgezette testen lijken onterecht gemarkeerd te zijn als onfaalbaar.
Test 3: Skip een aantal duplicate testen
In de vorige testen hadden we veel duplicate testen. Ik heb 10 duplicate testen uitgezet. Voor deze proof-of-concept heb ik uitsluitend simpele duplicaten gekozen. Dit zijn situaties waar twee testen precies dezelfde mutanten afdekken. Een van die twee testen heb ik uitgezet. Complexe duplicaten zijn te detecteren, maar het vereist meer implementatie effort dus ik laat het voor nu links liggen.
Het uitzetten van deze tien testen heeft de mutatie dekking niet significant veranderd. Het verschil valt binnen de meetonzekerheid. Deze data ondersteunt de hypothese. We hebben echter meer data nodig voordat we definitief kunnen stellen dat het verwijderen van duplicate testen geen impact heeft op de kwaliteit van de test suite.
Data samenvatting
De verzamelde data ondersteund de hypothese. De mutatie dekking is nauwelijks veranderd na het uitzetten van 23 testen. Maar de dataset is klein. We kunnen geen harde conclusies trekken voordat we meer data hebben verzameld.
Conclusie
De data die we tijdens het experiment hebben gegenereerd, vertelt ons dat we een tool kunnen maken die ons helpt met het verkleinen van onze low-level test suites zonder de kwaliteit significant aan te tasten. We kunnen dit doen met onze definitie van een overbodige test en de data gegenereerd door mutatie testen.
De bruikbaarheid van zo’n tool in de praktijk is discutabel. De codebases die er de meeste baat bij zouden hebben, zijn tegelijkertijd ook degene die de tool het moeilijkst kunnen gebruiken. De CPU-intensieve aard van mutation testen betekent dat zo’n tool uren of zelfs dagen nodig heeft. Tijdens dit experiment heb ik me meerdere keren afgevraagd of het het waard is om zo’n tool te bouwen en ik ben er nog steeds niet uit.
Zelfs als deze aanpak te CPU-intensief is voor dagelijks gebruik, is het misschien nuttig om af en toe te gebruiken. Het kan het waard zijn om deze tool één keer per jaar te draaien. Een test suite lente schoonmaak zeg maar. Denk jij dat het het waard is om deze tool te bouwen? Waar zou jij het voor gebruiken?
Reproduceer het zelf
- NodeJS 13.0
- Express commit 33e8dc303af9277f8a7e4f46abfdcb5e72f6797b
- StrykerJS 3.1 met configuratie:
{
“testRunner”: “mocha”,
“coverageAnalysis”: “perTest”,
“reporters”: [“html”, “progress”, “json”],
“ignoreStatic”: true
}
- Draai StrykerJS met npx stryker run
- Draai het onderstaande script met NodeJS
import { readFile } from “node:fs/promises”;
import { calculateMetrics } from “mutation-testing-metrics”; // A dependency of Stryker
const inputPath = “./reports/mutation/mutation.json”;
const input = JSON.parse(await readFile(inputPath, “utf-8”));
const metrics = calculateMetrics(input.files).metrics;
console.log(“Total mutants: ” + metrics.totalValid);
console.log(“Mutation coverage: ” + metrics.mutationScore.toFixed(3) + “%”);
// —————–
const allMutants = Object.values(input.files)
.map((file) => file.mutants)
.flat();
const allTests = Object.entries(input.testFiles)
.map(([filePath, file]) =>
file.tests.map((t) => {
return { …t, filePath };
})
)
.flat()
.map((test) => {
return {
…test,
killedMutants: allMutants
.filter((mutant) => mutant.coveredBy.includes(test.id))
.sort((a, b) => {
if (a.id > b.id) return -1;
if (a.id < b.id) return 1;
return 0;
}),
};
});
console.log(“Total tests: ” + allTests.length);
// —————–
const neverFailed = allTests.filter((t) => t.killedMutants.length === 0);
console.log(“Tests that can’t fail (0 mutants killed): ” + neverFailed.length);
// console.log(neverFailed);
// —————–
const testsWithDuplicates = allTests
.filter((t) => t.killedMutants.length > 0)
.map((thisTest) => {
return {
…thisTest,
duplicates: allTests.filter((otherTest) => {
if (otherTest.id === thisTest.id) {
return false;
}
if (otherTest.killedMutants.length !== thisTest.killedMutants.length) {
return false;
}
// Crude and inefficient way to compare which mutants are covered by each test
const thisKilledMutants = JSON.stringify(
thisTest.killedMutants.map((m) => m.id)
);
const otherKilledMutants = JSON.stringify(
otherTest.killedMutants.map((m) => m.id)
);
const match = thisKilledMutants === otherKilledMutants;
return match;
}),
};
})
.filter((test) => test.duplicates.length > 0);
console.log(
“Tests with at least 1 duplicate (same mutants covered): ” +
testsWithDuplicates.length
);
// console.log(testsWithDuplicates);