https://a.storyblok.com/f/270183/1368x665/24a2391523/26mar_dev-blog_php-pest-mut-test.jpg

Optimisez votre code PHP avec le test de mutation PEST

Publié le March 3, 2026

Temps de lecture : 5 minutes

PEST a été adopté par un grand nombre de personnes, en particulier parce qu'il est devenu la bibliothèque de test de facto pour Laravel ce qui permet aux développeurs Laravel de choisir entre PHPUnit ou PEST, avec PHPUnit sous le capot. Plus de choix ne peut être qu'une bonne chose, mais les taux d'adoption signifient également un cycle de vie de développement qui a vu beaucoup de nouvelles fonctionnalités de nouvelles fonctionnalités.

La rapidité avec laquelle les tests de mutation ont été adoptés a été surprenante, étant donné qu'il ne s'était pas passé grand-chose dans le monde des tests depuis la conception pilotée par les domaines et le développement guidé par le comportement. Je me souviens d'avoir été époustouflé par une démonstration de BDD analysant automatiquement les éléments suivants Gherkin et d'en extraire des tests par Ciaran McNulty à l'occasion de PHP Londres il y a quelques années. C'est quelque chose que je n'ai jamais vraiment fait professionnellement, mais une fois que les tests de mutation ont commencé à être adoptés par les développeurs PHP suite à la popularité de InfectionPHPj'ai su qu'il s'agissait d'une activité à laquelle j'attacherais beaucoup d'importance.

C'est ainsi que les tests de mutation sont de plus en plus répandus, Nuno Maduro a annoncé que la version 3.0 de PEST comporterait des tests de mutation. Dans cet article, nous allons nous y plonger avec un exemple de test utilisant l'API Vonage Messaging API.

Qu'est-ce que le test de mutation et pourquoi l'utiliser ?

Permettez-moi de commencer par dire que tous les outils que vous pouvez ajouter pour rendre votre code aussi robuste que possible seront toujours positifs. Vous pouvez donc l'ajouter à la liste des outils essentiels, tels que l'analyse statique avec des outils comme PsalmPHP ou PHPStanle linting avec quelque chose comme phpcsfixeret des tests robustes avec Test-Driven Development.

Nous sommes des êtres humains et, en tant que tels, nous commettons des erreurs dans nos tests (je sais que cela m'est arrivé). Il y a beaucoup de possibilités de se tromper - imaginez que vous êtes en train d'écrire un test et qu'il n'a pas d'assertions, vous voulez juste tester que n'importe quel code s'est exécuté sans lancer d'exception.

public function testService()
{
	$service = new \Vonage\ServiceContainer('dev');
	$service->runService();

	// If service fails, exception, so dummy completion assertion
	$this->assertTrue(true);
}

Il s'agit d'un faux positif. Bien sûr, vous pouvez voir le raisonnement derrière cela, mais vous avez aussi

100% Line Test Coverage

C'est techniquement correct, mais ce n'est pas un vrai test. Que se passe-t-il si le code sous-jacent change de telle manière que l'exception non documentée qui pourrait être levée ne peut plus se produire ?

Le test de mutation consiste à modifier le code sous-jacent, un thread à la fois (nous appelons ces threads des "mutants"). Si vous commencez à modifier le code, techniquement, tous vos tests devraient échouer. Nous avons déjà introduit l'élément humain dans le processus, en sachant que cela pourrait ne pas être le cas. Le résultat final que nous recherchons est que de très nombreux mutants ont été créés, mais qu'ils ont tous été "tués", c'est-à-dire que le test a échoué à cause de la mutation. Si vous avez des mutants qui "survivent", vos tests passent toujours. S'ils passent toujours, cela signifie que les tests n'ont pas remarqué de changements dans votre code. Ce n'est pas une bonne chose.

Notre référence : Test du SDK PHP de Vonage

Montrons d'abord comment nous mettons en œuvre un test régulier, en ayant un service Laravel imaginaire qui envoie un message RCS en utilisant Vonage.

<?php

namespace App\Services;

use Vonage\Client;

use Vonage\Messages\Channel\RCS\RcsText;

class RcsService
{

   public function __construct(
       private Client $vonage
   ) {}

   public function send(string $to, string $message): array
   {
       $response = $this->vonage->messages()->send(
           new RcsText($to, 'VonageApp', $message)
       );

       return $response;
   }
}

Pour simplifier, nous dirons que le conteneur de service de Laravel injecte un objet Vonage Client avec des informations d'identification dans le constructeur. Nous avons maintenant besoin d'un test qui n'effectue pas d'appel à l'API. Nous pouvons le faire en utilisant les Mocks.

use App\Services\RcsService;
use Vonage\Client;
use Vonage\Messages\Channel\RCS\RcsText;
use Vonage\Messages\Client as MessagesClient;

beforeEach(function () {
   $this->messagesClient = Mockery::mock(MessagesClient::class);
   $this->vonage = Mockery::mock(Client::class);
   $this->vonage->shouldReceive('messages')->andReturn($this->messagesClient);
});

afterEach(function () {
   Mockery::close();
});

test('Will send message using Vonage SDK to end number', function () {
   $response = ['message_uuid' => 'abc-123', 'to' => '33600000000'];

   $this->messagesClient
       ->shouldReceive('send')
       ->once()
       ->with(Mockery::type(RcsText::class))
       ->andReturn($response);

   $service = new RcsService($this->vonage);
   $result = $service->send('+33600000000', 'Hello world');
   expect($result)->toBeArray();
});

OK, le test est réussi. C'est bien. Mais que se passe-t-il si nous modifions le code ?

Le test de mutation est intégré à PEST 3, ce qui signifie que nous pouvons effectuer des mutations sur ce fichier dès maintenant et voir ce qui se passe. Il existe de nombreuses options de configuration pour PEST, mais la manière la plus simple d'ordonner aux mutations de se produire est d'utiliser la fonction covers() pour demander aux mutations de se produire. Dans ce cas, la ligne suivante est ajoutée au fichier de test :

use App\Services\RcsService;

covers(RcsService::class);

Cela indique à PEST que lors de l'exécution de ce fichier de test, il faut aller de l'avant et faire muter le code dans RcsService. Pour exécuter les mutations de PEST avec un seuil de réussite de 100% :

./vendor/bin/pest --mutate --min=100

Nous obtenons le résultat suivant :

 Mutating application files...

  1 Mutations for 1 Files created

   RUN  app/Services/RcsService.php

  ⨯ Line 20: AlwaysReturnEmptyArray

  ---------------------------------------------------------------------------------------------------------------------------  

   UNTESTED  app/Services/RcsService.php  > Line 20: AlwaysReturnEmptyArray - ID: 5befe24d3b8d7f6f

       public function send(string $to, string $message): array

       {

           $response = $this->vonage->messages()->send(new RcsText($to, 'VonageApp', $message));

  -        return $response;

  +        return [];

       }

   }

  

  Mutations: 1 untested, 0 tested

  Score:     0.00%

  Duration:  0.30s

   FAIL  Mutation score below expected: 0.0 %. Minimum: 100.0 %.

Quelque chose ne va pas. Mais qu'est-ce qui ne va pas ? Aha ! Voici le formidable pouvoir du test de mutation, expliqué dans les étapes suivantes :

  • PEST modifie le RcsService() pour renvoie toujours un tableau vide

  • PEST exécute le fichier de test en tant que mutant

  • Nous voulons que le mutant échoue, et donc soit tué

  • Il ne le fait pas, il passe

  • Cela signifie que votre test est trop faible

Alors, pourquoi est-elle si faible ? La conclusion est la suivante : vous devriez tester les données utiles renvoyées par Vonage. Outre le fait que l'API de Vonage ne renvoie jamais un tableau vide, vous devez tester les données utiles renvoyées. En utilisant ->toBeArray() dans ce cas n'est pas suffisante.

Remédier à vos faiblesses

Le test définit en fait la sortie fictive qui est donnée, donc on peut supposer que ce que nous testons ici est que la charge utile renvoyée (que nous avons fournie) n'est pas modifiée de quelque manière que ce soit. Comme nous avons défini $responsenous devrions modifier le test pour nous assurer qu'il correspond à cette structure de données exacte.

expect($result)->toBe($response);

C'est tout. Une seule ligne, juste pour changer l'assertion. En exécutant à nouveau les mutations PEST, nous obtenons ce qui suit :

Tests:    1 passed (2 assertions)

  Duration: 0.26s

  Mutating application files...

  1 Mutations for 1 Files created

   RUN  app/Services/RcsService.php

  ✓ Line 20: AlwaysReturnEmptyArray

  Mutations: 1 tested

  Score:     100.00%

  Duration:  0.34s

Wow.

Conclusion

Je sais qu'il s'agit d'un exemple assez basique pour commencer. Cependant, le concept de ce que les tests de mutation fournissent est principalement ce que nous couvrons ici. Bien qu'il ne s'agisse que de quelques lignes de code, cet exemple démontre la puissance de ce que les tests de mutation peuvent faire pour renforcer votre base de code. Si vous l'utilisez avec quelque chose comme PHPStan, les développeurs PHP n'ont jamais été aussi bien lotis pour livrer un code renforcé.

Vous avez une question ou souhaitez partager ce que vous construisez ?

Restez connecté et tenez-vous au courant des dernières nouvelles, astuces et événements concernant les développeurs.

Partager:

https://a.storyblok.com/f/270183/400x385/12b3020c69/james-seconde.png
James SecondeDéveloppeur PHP senior Advocate

Acteur de formation avec une thèse sur la comédie, je suis venu au développement PHP par le biais de la scène des rencontres. Vous pouvez me trouver en train de parler et d'écrire sur la technologie, ou de jouer/acheter des disques bizarres de ma collection de vinyles.