• 8 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 5/12/22

Créez des doublures

Vous vous sentez déjà plus à l'aise avec les tests unitaires, non ? Il est temps d'aller plus loin avec du code plus difficile et plus complexe à tester.

Disons que vous arrivez sur un projet que vous n'avez pas développé, et que l'on vous demande de tester de façon unitaire tout le code en rapport avec l'authentification d'un utilisateur.

L'authentification passe par GitHub… Alors, comment faire ?

Ne dépendez pas d'un système externe

Le principe d'un test unitaire est surtout de ne pas dépendre d'un système externe. Il va falloir donc créer une doublure de GitHub.

Qu'est-ce qu'une doublure ?

Une doublure est un élément que l’on crée de toutes pièces pour maîtriser une dépendance externe. Cela permet de ne pas dépendre du bon fonctionnement de GitHub (dans le cas où l'élément extérieur est GitHub).

Encore un peu de vocabulaire avant de vous lancer dans le code !

On utilisera ces trois types de doublures dans le cours :

  1. Mock.

  2. Dummy.

  3. Stub.

Mock

Le but est d'effectuer tranquillement vos tests unitaires sur une méthode qui a besoin de ce type de classe.

Bonne nouvelle, PHPUnit permet d'utiliser des mocks facilement ! Vous pouvez donc demander à PHPUnit de vous fournir un objet du type de classe dont vous avez besoin.

Nous reviendrons au concept de mock juste après vous avoir expliqué les mots “dummy” et “stub”.

Dummy

Prenons un exemple.

Le code à exécuter demande un objet de type  GuzzleHttp\Client  .

Dans notre test unitaire, vous aurez donc à écrire ceci :

<?php
namespace App\Tests;
use PHPUnit\Framework\TestCase;
class ClassTest extends TestCase
{
   public function testExemple()
   {
      $client = $this->getMock('GuzzleHttp\Client');
      // …
   }
}

Dans la variable  $client  , vous pouvez repérer une instance un peu particulière, utilisable comme doublure d'un objet.

Stub

Concrètement, cela veut dire que vous indiquez ce que la méthode d'un objet doit toujours retourner lorsqu'elle est appelée.

Reprenons l'exemple avec notre dummy qui a le type  GuzzleHttp\Client  .

Transformons-le en stub. Pour cela, il faut indiquer que, lorsque la méthode get de la classe est appelée, un objet de type  Symfony\Component\HttpFoundation\Request  devra toujours être retourné.

<?php
namespace App\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
class ClassTest extends TestCase
{
   public function testExemple()
   {
      $request = new Request();
      $client = $this->getMock('GuzzleHttp\Client');
      $client->method('get')->willReturn($request);
      // …
   }
}

Et voilà ! Notre “dummy”  $client  est devenu “stub”. C'est aussi simple que ça.

Il est tout à fait possible de faire en sorte que la méthode get retourne à son tour un dummy. Pour ce faire, il suffit de remplacer la ligne :

       $request = new Request();

par :

      $request = $this->getMock('Symfony\Component\HttpFoundation\Request');

Insérez une assertion dans un mock

Je vous ai dit tout à l'heure qu'un mock était une doublure. C'est vrai bien sûr, mais je vais maintenant être un peu plus précise. Un mock est un stub qui a des attentes (“expectations”, en anglais). Avec un mock, quand vous allez appeler une méthode, vous pouvez préciser le comportement attendu lors de cet appel (par exemple, vous allez appeler la méthode une seule fois). 

Reprenons notre exemple, nous allons ajouter une attente :

<?php
namespace App\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
class ClassTest extends TestCase
{
   public function testExemple()
   {
      $request = new Request();
      $client = $this->getMock('GuzzleHttp\Client');
      $client
         ->expects($this->once())
         ->method('get')
         ->willReturn($request);
      // …
   }
}

On a ajouté à notre stub :

$client

         ->expects($this->once())
         ->method('get')
         ->willReturn($request);

On s’attend à ce que la méthode  get  soit appelée une fois au moment où le code à tester sera exécuté (en plus de retourner un objet  Request  ).

Cette attente est considérée comme une “assertion” dans votre suite de tests. Si la méthode n'est pas appelée lorsque le code à tester est exécuté, la suite de tests échoue.

Doublez un objet

Vous pourrez avoir besoin de doubler un objet pour plusieurs raisons. En voici deux :

  1. Quand un objet est difficile à instancier.

  2. Pour maîtriser le retour d’une méthode appelée par le code original.

Raison n° 1 : un objet est difficile à instancier

Si vous avez besoin de tester les méthodes d'une classe qui requiert des dépendances difficiles à construire, il est pratique de créer une doublure au lieu d'instancier la dépendance en question.

Prenons l'exemple d'une classe difficile à instancier :  JMS\Serializer\Serializer  .

<?php
namespace JMS\Serialiser;
// …
class Serializer
{
   //…
   public function __construct(
      MetadataFactoryInterface $factory,
      HandlerRegistryInterface $handlerRegistry,
      ObjectConstructorInterface $objectConstructor,
      MapInterface $serializationVisitors,
      MapInterface $deserializationVisitors,
      EventDispatcherInterface $dispatcher = null,
      TypeParser $typeParser = null,
        ExpressionEvaluatorInterface $expressionEvaluator = null
   )
   {
      //…
}

Comme vous pouvez le constater, le constructeur de cette classe est affreusement compliqué !

C'est pourquoi, si vous avez besoin d'une instance du  Serializer  dans un test, ce serait plus facile de créer un dummy en demandant à PHPUnit de ne pas faire appel au constructeur original :

<?php
namespace App\Tests\Folder;
use PHPUnit\Framework\TestCase;
class ExempleClassTest extends TestCase
{
   public function testExemple()
   {
      $serializer = $this
         ->getMockBuilder('JMS\Serializer\Serializer')
         ->disableOriginalConstructor()
         ->getMock();
      $classToTest = new ExempleClass($serializer);
      // …
   }
}

Et voilà ! Avec ces lignes, vous avez un serializer disponible et malléable, à utiliser dans vos tests autant que vous le souhaitez.

Raison n° 2 : Maîtriser le retour d'une méthode appelée par le code original

Quand vous exécutez le code à tester lors d'un test unitaire, il peut arriver que vous ayez besoin de maîtriser ce qui est retourné lors d'une instruction.

Par exemple, disons que l'une des instructions à exécuter ressemble à ce qui suit :

<?php
namespace AppBundle\Security;
class GithubUserProvider extends UserProviderInterface
{
   private $client;
   public function __construct(Client $client)
   {
      $this->client = $client;
   }
   public function loadUserByUsername($username)
   {
                                        $response = $this->client->get('https://api.github.com/user?access_token='.$username);
      // …
   }
}

À la ligne :

                                       $response = $this->client->get('https://api.github.com/user?access_token='.$username);

… nous effectuons une requête HTTP GET auprès de GitHub (le service externe dans notre exemple). Or, dans un test unitaire, vous devez faire attention à ne pas être tributaire d'une communication auprès d'un service externe, qui pourrait être défaillante.

La solution est de doubler l'objet  $client  . Ainsi, lorsque le code exécuté atteint la ligne :

                                      $response = $this->client->get('https://api.github.com/user?access_token='.$username);

… le contenu de la variable  $response  est maîtrisé.

Pour ce faire, il vous suffit de faire un stub comme suit :

<?php
namespace App\Tests\Folder;
use App\Security\GithubUserProvider;
use PHPUnit\Framework\TestCase
class GithubUserProviderTest extends TestCase
{
   public function testLoadUserByUsername()
   {
      $response = …; // Ce que l'on souhaite recevoir.
                                        $client = $this->getMockBuilder('GuzzleHttp\Client')
         ->disableOriginalConstructor()
         ->getMock();
      $client
         ->method('get')
         ->willReturn($response);
      // …
                          $githubUserProvider = new GithubUserProvider($client);
      $githubUserProvider->loadUserByUsername('xxxxx');
      // Assertions du test
      // …
   }
}

Reprenons.

Comme vous le voyez, on a  créé un objet  $client :

$client = $this->getMockBuilder('GuzzleHttp\Client')
         ->disableOriginalConstructor()
         ->setMethods(['get'])
         ->getMock();
  1. Avec la méthode  disableOriginalConstructor  , on demande à PHPUnit de ne pas faire appel au constructeur original de la classe  GuzzleHttp\Client  .

  2. Puis, avec la méthode  setMethods  , on indique quelle méthode existe dans cette classe.

Une dernière chose à faire… Lorsque la méthode  get  est appelée, on précise bien ce qui doit être retourné, en l'occurrence ce que contient la variable  $response  définie à la ligne :

      :$response = …; // Ce que l'on souhaite recevoir..

Vous serez bien évidemment confronté à d'autres cas de figure. Cependant, si vous avez bien compris l'intérêt d'une doublure, vous n'aurez pas de mal à créer le code de la situation. L'intérêt d'une doublure, c'est donc de maîtriser entièrement ce qu'il se passe à un ou plusieurs moments du code à tester, pour ne pas dépendre d'éléments susceptibles d'entraver le test unitaire.

Expérimentez avec la librairie Prophecy

Expérimentez-la ! Elle offre une manière différente d'appréhender les mocks dans ses tests.

Cela ne demande aucune installation, étant donné que PHPUnit l'embarque déjà.

En résumé

  • Pour que les tests unitaires soient efficaces, il ne faut pas être tributaire d'une communication auprès d'un service externe, qui pourrait être défaillante. Nous restons maître de nos tests en toutes circonstances.

  • Un mock est une doublure.

  • Un dummy est un mock qui remplit un contrat.

  • Un stub est un dummy auquel on ajoute un comportement.

  • Un mock est un stub qui a des attentes.

  • Il faut penser à désactiver le constructeur de l’objet avec la fonction disableOriginalConstructor.

Rendez-vous au chapitre suivant pour tester une classe complexe, comprenant de nombreuses dépendances. Allez, on y va !

Example of certificate of achievement
Example of certificate of achievement