• 12 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 29/05/2019

Les doublures (mocks)

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

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 unitairement tout le code en rapport avec l'authentification d'un utilisateur. Néanmoins, nous savons déjà que l'authentification passe par Github… Comment faire ?

Ne pas dépendre d'un système externe dans le contexte des tests

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. Oui oui, une doublure comme au cinéma ! :D

Qu'est-ce qu'une doublure ?

Une doublure est un élément que nous aurons créé de toutes pièces pour maîtriser une dépendance externe. Cela nous permet de ne pas du tout 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 !

Mock

Un mock est une doublure. :zorro: C'est un objet créé à partir d'un type de classe dont vous maîtrisez entièrement le comportement. 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.

Je reviendrai au concept de mock juste après vous avoir expliqué les mots "dummy" et "stub".

Dummy

Un dummy est un objet un peu particulier qui remplit un contrat.

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 Tests\AppBundle;

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.

Attention, si une méthode est appelée sur cet objet, null sera retourné.

Stub

Un stub est un dummy auquel on ajoute un comportement. Cela signifie concrètement 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 st appelée, un objet de type Symfony\Component\HttpFoundation\Request devra toujours être retourné.

<?php

namespace Tests\AppBundle;

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 11 par $request = $this->getMock('Symfony\Component\HttpFoundation\Request');.

Mock et assertion

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 Tests\AppBundle;

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);
        
        // …
    }
}

J'ai ajouté la ligne 15 à notre stub : ce que je dis avec cela, c'est que je m'attends à 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ée, la suite de tests échouera.

Quelques cas requérant la création d'une doublure

Vous pourrez avoir besoin de doubler un objet pour plusieurs raisons. En voici quelques unes.

Raison n°1 : Un objet est difficile à instancier

Dans le cas où vous aurez 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 Tests\AppBundle\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 à l'envie dans vos tests. :soleil:

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

Lorsque vous exécutez le code à tester lors d'un test unitaire, il peut arriver que vous ayez besoin de maîtriser ce qui 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 16, 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 de ne pas être tributaires 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é atteindra la ligne 16, le contenu de la variable $response est maîtrisé.

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

<?php

namespace Tests\AppBundle\Folder;

use AppBundle\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()
            ->setMethods(['get'])
            ->getMock();
        $client
            ->method('get')
            ->willReturn($response);
        // …
        
        $githubUserProvider = new GithubUserProvider($client);
        $githubUserProvider->loadUserByUsername('xxxxx');
        
        // Assertions du test
        // …
    }
}

Je reprends. :) Comme vous le voyez, j'ai créé un objet $client (lignes 13 à 16). Avec la méthode disableOriginalConstructor, je demande à PHPUnit de ne pas faire appel au constructeur original de la classe GuzzleHttp\Client. Puis, avec la méthode setMethods, j'indique quelle méthode existe dans cette classe.

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

Si vous souhaitez aller plus loin avec les doublures (mocks), je vous invite à lire la documentation officielle de PHPUnit à ce sujet.

Une autre librairie de mock à expérimenter : Prophecy

Il existe une librairie de mock déjà intégrée à PHPUnit : Prophecy. Je vous invite vivement à l'expérimenter ! Elle offre une manière différente (et selon moi, plus simple) d'appréhender les mocks dans ses tests.

Faites un essai ! Cela ne demande aucune installation étant donné que PHPUnit l'embarque déjà.

Rendez-vous au chapitre suivant pour tester une classe complexe, comprenant de nombreuses dépendances.

Exemple de certificat de réussite
Exemple de certificat de réussite