Mutation testing : mesurer ce que valent vos tests
La couverture dit quelles lignes sont exécutées, pas lesquelles sont vérifiées. Ce que le mutation testing mesure réellement, ce qu'il coûte en temps machine et en attention, et les zones du code où il rapporte.
Par Elias Varen7 min de lectureMembres
Soit une règle métier et son test, avec une couverture de lignes et de branches de 100 %.
final class DiscountPolicy
{
public function rateFor(int $quantity): int
{
if ($quantity >= 10) {
return 15;
}
return 0;
}
}
// Dans la classe de test
public function testDiscount(): void
{
$policy = new DiscountPolicy();
self::assertSame(15, $policy->rateFor(50));
self::assertSame(0, $policy->rateFor(1));
}Si l'on remplace >= par >, les deux assertions passent toujours. Si l'on remplace 10 par 11, ou par 20, elles passent encore. Le seuil, c'est-à-dire la seule chose que cette règle décide, n'est vérifié par rien. Un client qui commande exactement dix unités peut perdre sa remise sans qu'aucun test ne bouge, et votre tableau de bord affiche 100 %.
La couverture mesure ce que les tests exécutent. Le mutation testing mesure ce qu'ils remarquent. Ce sont deux grandeurs différentes, et seule la seconde a un rapport avec la raison pour laquelle on écrit des tests.
Tué, survivant, non couvert : le mécanisme
L'outil (Infection pour PHP, Stryker pour TypeScript) applique au code une petite modification à la fois, appelée mutant. Il inverse un opérateur de comparaison, remplace + par -, change true en false, supprime un appel de méthode, vide un tableau retourné, retire un return anticipé. Pour chaque mutant, il lance les tests.
mutant généré ──▶ aucun test n'exécute la ligne → non couvert
──▶ un test échoue → tué
──▶ tous les tests passent → survivant
──▶ les tests ne terminent pas → timeoutUn mutant tué prouve qu'au moins un test dépend du comportement modifié. Un survivant est une modification du code que votre suite accepte sans broncher, ce qui signifie qu'un test manque, qu'une assertion est trop faible, ou que le code modifié ne sert à rien.
Le score est le rapport des mutants tués sur le total, et il a deux variantes qu'il ne faut pas confondre. Calculé sur tous les mutants, il mélange les trous de couverture et la faiblesse des assertions ; calculé sur les seuls mutants couverts, il isole la seconde. C'est cette variante qui apporte une information que vous n'aviez pas déjà.
À lire ensuite
Toute la rubrique ArchitectureWeb
Accessibilité : ce que l'automatisation ne trouve pas
Un analyseur automatique vérifie ce qui se décide en lisant le DOM, et ignore ce qui se passe quand quelqu'un utilise la page. Ce qui lui échappe par construction, et un protocole manuel de trente minutes par parcours pour le couvrir.
8 minMembres
Architecture
Tests de caractérisation : figer un comportement qu'on ne comprend pas
Avant de toucher à du code que personne ne comprend, on enregistre ce qu'il fait, pas ce qu'il devrait faire. Comment construire ce filet, le rendre déterministe, savoir ce qu'il ne couvre pas, et quand le jeter.
7 minMembres
Architecture
Simulation déterministe pour équipes ordinaires
Sans réécrire votre système autour d'un simulateur, vous pouvez rendre reproductibles les bugs d'ordre, de doublon et de panne partielle. Ce qui est atteignable avec une horloge injectée, un aléa à graine et un bus simulé, ce que ça coûte, et où ça s'arrête.
7 minMembres