Retour sur le Forum PHP 2024
Découvrez un résumé concis des conférences qui nous ont le plus marqué lors du Forum PHP 2024 !
Sommaire
Ce n’est plus à démontrer : les tests unitaires sont incontournables dans le développement d’une application. Ils permettent de mettre en évidence d’éventuelles régressions apportées lors de modifications du code, et donc au développeur d’acquérir une certaine confiance à mettre le code en production : si les tests passent, c’est que tout fonctionne correctement.
Pour mesurer cette confiance, on utilise principalement comme métrique la couverture de code. Plus la couverture est grande (proche de 100%), moins il y a de chances qu’une régression passe entre les mailles du filet. Mais attention ! Cette affirmation n’est que purement théorique !
Nous allons voir que dans certains cas la couverture de code n’est qu’un faux indicateur de protection. Voici un exemple simple :
<?php
class Astronaut {}
class SpaceShip
{
private $capacity;
public $astronauts = [];
public function __construct($capacity)
{
$this->capacity = $capacity;
}
public function addAstronaut(Astronaut $astronaut)
{
if (count($this->astronauts) < $this->capacity) {
$this->astronauts[] = $astronaut;
}
}
}
Ici notre classe SpaceShip
a une méthode publique addAstronaut
qui ajoute une instance de Astronaut
uniquement si la capacité maximale n’est pas atteinte.
Voyons un exemple de test unitaire associé :
<?php
class SpaceShipTest extends \PHPUnit_Framework_TestCase
{
public function testAddAstronaut()
{
$spaceShip = new SpaceShip(1);
$spaceShip->addAstronaut(new Astronaut());
$this->assertCount(1, $spaceShip->astronauts);
}
}
Le test vérifie ici que la méthode ajoute bien une entrée au tableau d’astronautes. En lançant les tests nous avons une couverture de 100% (même sans assertion nous aurions eu ce résultat).
Mais nous ne sommes pas protégés pour autant : que se passerait-il si la méthode addAstronaut
changeait ?
Notre test suffira-t-il à détecter une régression ?
Pour détecter les failles dans vos tests unitaires, il existe une solution : les tests de mutation.
Le principe est simple : altérer le code source pour vérifier que les tests associés échouent en conséquence. Afin d’y parvenir, voici les étapes nécessaires :
Mais avant de voir ça de plus près, voici un peu de vocabulaire :
!==
remplacé par un ===
)Ici nous utiliserons Humbug, un framework parmi d’autres qui permet de faire des tests de mutation en PHP.
Lorsque nous lançons Humbug avec notre exemple de tout à l’heure, nous obtenons :
humbug ... Mutation Testing is commencing on 1 files... (.: killed, M: escaped, S: uncovered, E: fatal error, T: timed out) M. 2 mutations were generated: 1 mutants were killed 0 mutants were not covered by tests 1 covered mutants were not detected 0 fatal errors were encountered 0 time outs were encountered Metrics: Mutation Score Indicator (MSI): 50% Mutation Code Coverage: 100% Covered Code MSI: 50%
Diantre ! Un mutant nous a échappé ! Voyons dans le fichier de de log :
1) \Humbug\Mutator\ConditionalBoundary\LessThan Diff on \SpaceShip::addAstronaut() in src/SpaceShip.php: --- Original +++ New @@ @@ { - if (count($this->astronauts) < $this->capacity) { + if (count($this->astronauts) <= $this->capacity) { $this->astronauts[] = $astronaut; } } }
Nos tests n’ont pas détecté le changement d’opérateur de comparaison. En effet, nous n’avons pas testé le cas où notre vaisseau spatial est plein. À présent, ajoutons un test pour couvrir ce use-case :
<?php
class SpaceShipTest extends \PHPUnit_Framework_TestCase
{
public function testAddsAstronautWhenShipNotFull()
{
$spaceShip = new SpaceShip(1);
$spaceShip->addAstronaut(new Astronaut());
$this->assertCount(1, $spaceShip->astronauts);
}
public function testDoesNotAddAstronautWhenShipFull()
{
$spaceShip = new SpaceShip(0);
$spaceShip->addAstronaut(new Astronaut());
$this->assertCount(0, $spaceShip->astronauts);
}
}
Maintenant relançons Humbug :
humbug ... Mutation Testing is commencing on 1 files... (.: killed, M: escaped, S: uncovered, E: fatal error, T: timed out) .. 2 mutations were generated: 2 mutants were killed 0 mutants were not covered by tests 0 covered mutants were not detected 0 fatal errors were encountered 0 time outs were encountered Metrics: Mutation Score Indicator (MSI): 100% Mutation Code Coverage: 100% Covered Code MSI: 100%
Et voilà, cette fois aucun mutant ne s’est échappé, notre suite de tests est vraiment efficace, ce bug éventuel n’arrivera jamais jusqu’à la production ! Evidemment, l’exemple choisi ici est volontairement simple et n’est pas très évocateur, mais dans le code métier au cœur de votre application, vous avez certainement des use-case beaucoup plus sensibles.
Pour parvenir à ses fins, Humbug est capable de générer tout un éventail de mutations :
>
par >=
, !==
par ===
, etc…)0
par 1
, true
par false
, etc…)&&
, ||
, etc…)&
, |
, %
, etc…)Les tests de mutation sont un moyen simple et efficace de détecter la fiabilité des tests unitaires. La couverture de code n’est pas une métrique très fiable, un code peut être couvert à 100% sans une seule assertion ! Nous avons vu avec Humbug que nous pouvons automatiser ces tests, il devient alors possible de les greffer dans notre workflow d’intégration continue. Attention toutefois au temps d’exécution qui grandit de manière exponentielle lorsque la base de code grandit, on utilisera en priorité les tests de mutation là où il y a un véritable enjeu : le code métier.
Auteur(s)
Robin Graillon
Robin est un développeur PHP/Symfony passionné. Il aime découvrir de nouvelles technologies, pratiquer plusieurs langages, et parler de lui à la 3ème personne.
Vous souhaitez en savoir plus sur le sujet ?
Organisons un échange !
Notre équipe d'experts répond à toutes vos questions.
Nous contacterDécouvrez nos autres contenus dans le même thème
Découvrez un résumé concis des conférences qui nous ont le plus marqué lors du Forum PHP 2024 !
Le composant Symfony ExpressionLanguage : qu'est-ce que c'est ? Quand et comment l'utiliser ? Comment créer des expressions lors de cas plus complexes ?
Découvrez comment réaliser du typage générique en PHP : introduction et définition du concept, conseils et explications pas-à-pas de cas pratique.