Améliorez vos tests avec Infection

Publié le 16 août 2023 par Mathieu De Gracia
Couverture de l'article Améliorez vos tests avec Infection

Plus que jamais, écrire des tests automatisés dans nos applications est primordiale pour la qualité et la bonne santé de notre code, les tests étant même un sujet régulier de ce blog.

Mais dans le fond, pourquoi écrivons-nous des tests ?

Nous écrivons principalement des tests pour nous protéger des régressions, pour s'assurer qu'un changement dans notre codebase ne provoque pas d'effet de bord inattendu ou indésirable.

En d’autres mots : nous figeons des comportements à travers nos tests.

Le test étant essentiel, il sera interessant de vous assurez de leur qualité, vous pourriez vous en remettre au pourcentage de coverage mais cette valeur est loin d’être absolue et uniquement representative de la couverture de vos tests.

Avoir un coverage de 100% n’est aucunement révélateur de la pertinence et de la qualité de ces derniers.

Infection est un package de Mutation Testing vous permettant de vérifier la capacité de vos tests à detecter un changement dans votre code, voyons ensemble comme l'utiliser dans vos projets !

Installation

Une installation par composer est possible mais nous vous conseillons vivement d'utiliser le phar mis à disposition, vous pourrez ainsi utiliser le package sans vous souciez de satisfaire les contraintes des packages de votre projet :

1wget https://github.com/infection/infection/releases/download/0.27.0/infection.phar

Une fois téléchargé, lancez la commande suivante pour procéder à l'installation et à votre première analyse :

1php infection.phar

Lors de l'installation, Infection vous demandera de choisir les dossiers à inclure dans son analyse, dans notre cas nous choisirons le dossier /app de Laravel :

1Which source directories do you want to include (comma separated)? [app,database/factories,database/seeders]:
2 [0 ] .
3 [1 ] app
4 [2 ] bootstrap
5 [3 ] config
6 [4 ] database
7 [5 ] node_modules
8 [6 ] public
9 [7 ] resources
10 [8 ] routes
11 [9 ] storage
12 [10] tests
13 [11] vendor
14 [13] database/factories
15 [14] database/seeders
16 > 1

Dès lors, Infection procédera à sa première analyse !

Attention, il est extrêmement important que vos tests fonctionnent correctement avant d'utiliser Infection, dans le cas contraire le package sera dans l'impossibilité d'analyser votre projet.

Une histoire de mutation

Infection est un package dit de "fault-based testing" que l'on pourrait tout simplement traduire par "Test basé sur les défauts", un défaut étant généralement une modification mineure du code entrainant un comportement inattendu de votre application.

Dans Infection, ces défauts seront dénommés des mutants. 😈

Une mutation consiste à introduire délibérément une modification dans votre code dans le but de provoquer une réaction de vos tests afin d'en vérifier leur exhaustivité, imaginez la fonction suivante :

1function greaterThan(int $a, int $b): bool
2{
3 return $a > $b;
4}

Ce code est actuellement couvert par le test suivant vérifiant son bon comportement :

1public function it_able_to_determine_that_number_is_greater(): void
2{
3 $result = greaterThan(3, 2);
4 
5 $this->assertTrue(result);
6}

En lançant une analyse, Infection sera capable de modifier le code source de la méthode greaterThan pour y injecter des mutations, par exemple, en modifiant la priorité de vérification des variables :

1public function greaterThan(int $a, int $b): bool
2{
3 return $a >= $b;
4}

Une fois votre code muté, Infection exécutera de lui-même vos tests dans l'espoir d'y détecter une réaction.

Si vos tests automatisés fonctionnent normalement et sont incapables de détecter ce changement alors la mutation en question sera considérée comme survivante.

Ces survivants seront tout autant de modification prouvant que vos tests ne sont pas suffisamment complets pour y détecter des changements susceptibles d'ouvrir la voie à de futures régressions.

Un cas d'utilisation que vos tests ne prévoient pas est un bug en devenir.

Comprendre l'analyse

Le résultat d'une analyse de Infection est quelque peu déstabilisant au premier abord, voyons comment demystifier tout ca.

1221 mutations were generated:
2 115 mutants were killed
3 0 mutants were configured to be ignored
4 49 mutants were not covered by tests
5 57 covered mutants were not detected
6 0 errors were encountered
7 0 syntax errors were encountered
8 0 time outs were encountered
9 0 mutants required more time than configured
10
11Metrics:
12 Mutation Score Indicator (MSI): 52%
13 Mutation Code Coverage: 77%
14 Covered Code MSI: 66%

Dans un premier temps, parmi tous les états possibles d'un mutant suite à une analyse, seuls 3 sont véritablement importants :

Les autres états sont de l'ordre du débogage : fatal error, syntax error, timed out ...

Ces états permettent de calculer diverses métriques, mais l'une d'entre elles se démarque tout particulièrement : le MSI.

Le MSI, pour Mutation Score Indicator, est une métrique calculée à l'aide de la formule suivante :

1TotalDefeatedMutants = KilledCount + TimedOutCount + ErrorCount;
2
3MSI = (TotalDefeatedMutants / TotalMutantsCount) * 100;

Lors de notre analyse, la valeur de MSI fut seulement de 52%, cela signifie que 48% des mutants générés n'ont pas été détectés et éliminés par nos tests ... alors que cette application est couverte à plus de 80% d'après PHPUnit !

Dans notre cas, cette valeur de MSI est relativement faible et synonyme d'une couverture de test encore bien trop lacunaire et probablement dans l'incapacité de vous protéger de futures régressions.

Pour finir, vous pouvez lors d'une analyse visualiser les mutants en question à l'aide de l'option --show-mutations :

1php infection.phar --show-mutations

Configurer les mutants

Infection peut parfois se montrer capricieux, il sera par moments judicieux pour mener à bien vos analyses de restreindre le nombre de mutants utilisé.

Ce paramétrage est possible grace à un fichier de configuration dénommé infection.json5 qui fut automatiquement créé à l'installation du package et présent à la racine de votre projet.

Actuellement, la totalité des mutateurs sont activés ce qui peut provoquer des plantages et un temps conséquent d'analyse :

1"mutators": {
2 "@default": true
3}

Libre à vous de sélectionner les mutants qui vous intéressent, par exemple, si vous souhaitez uniquement inverser la valeur des booleans présents dans votre projet lors de l'analyse (true -> false / false -> true) :

1"mutators": {
2 "TrueValue": true,
3 "FalseValue": true
4}

Nous vous conseillons vivement de parcourir la documentation afin de vous constituer un ensemble de mutator qui vous correspondra le mieux et qui aura le plus de chance de réussir une analyse !

Vous retrouvez la liste des mutants disponibles à cette URL.

Conclusion

Infection est un outil complémentaire qui ne testera pas à proprement parler votre code mais garantira la qualité de votre pyramide de tests en vous assurant qu'elle vous protègera de potentielles régressions.

C'est un outil parfois difficile à manier et qui pourra échouer de manière incohérente et aléatoire ... cependant, l'effort que vous y investirez sera sans aucun doute extrêmement bénéfique pour améliorer la qualité et la pertinence de vos tests !

Mathieu De Gracia avatar
Mathieu De Gracia
Des fois, mon chat code à ma place

A lire

Autres articles de la même catégorie