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 :
Mock.
Dummy.
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 :
Quand un objet est difficile à instancier.
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();
Avec la méthode
disableOriginalConstructor
, on demande à PHPUnit de ne pas faire appel au constructeur original de la classeGuzzleHttp\Client
.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 !