Vous avez manipulé des doublures dans le chapitre précédent : il est temps de vous confronter à une classe contenant du code complexe. Ne vous inquiétez pas, nous allons nous exercer ensemble !
Pour le TP qui va suivre, vous allez tester une application dans laquelle on a implémenté une authentification avec GitHub. Rassurez-vous, on vous fournit l'application fonctionnelle. Le but est de tester le code, après tout !
Voici la classe que vous allez tester. Vous pouvez déjà repérer une méthode dont la logique métier demande de nombreuses dépendances :
<?php
// src/Security/GithubUserProvider.php
namespace App\Security;
use App\Entity\User;
use GuzzleHttp\Client;
use JMS\Serializer\Serializer;
use Symfony\Component\Security\Core\User\UserProviderInterface;
//…
class GithubUserProvider implements UserProviderInterface
{
private $client;
private $serializer;
public function __construct(Client $client, Serializer $serializer)
{
$this->client = $client;
$this->serializer = $serializer;
}
public function loadUserByUsername($username) : User
{
$response = $this->client->get('https://api.github.com/user?access_token='.$username);
$result = $response->getBody()->getContents();
$userData = $this->serializer->deserialize($result, 'array', 'json');
if (!$userData) {
throw new \LogicException('Did not managed to get your user info from Github.');
}
$user = new User(
$userData['login'],
$userData['name'],
$userData['email'],
$userData['avatar_url'],
$userData['html_url']
);
return $user;
}
// …
}
Vous allez passer par 4 étapes pour réaliser ce TP :
Déterminer ce qu’il faut tester.
Initier le test et exécuter le code à tester retournant un objet
User
.Écrire le test couvrant le cas où aucune information ne serait récupérée après la requête HTTP à GitHub.
Factoriser le code.
Maintenant, à vous de jouer !
Déterminez ce qu'il faut tester
Avec ce code, vous êtes dans une situation courante : vous arrivez sur un projet et lisez du code que vous n'avez pas écrit vous-même. Il va donc vous falloir parcourir le code et identifier les éléments à tester.
L’objectif de cette première étape est de déterminer quelles sont les dépendances, et de comprendre ce que la méthode à tester doit retourner.
Vous avez une classe avec deux dépendances :
Une instance de
GuzzleHttp\Client
.Et une instance de
JMS\Serializer\Serializer
.
Maintenant, penchons-nous sur ce que la méthode à tester doit retourner.
Dans le code de la méthode loadUserByUsername
ci-dessus, il y a deux issues possibles :
Soit retourner un objet
$user
de typeAppBundle\Entity\User
.Soit lancer une exception de type
LogicException
si la variable$userData
est vide.
Vous aurez donc deux tests à écrire.
Allez-y, essayez de le faire !
Si vous n’y arrivez pas, pas d'inquiétude : on vous accompagne étape par étape !
Voici donc comment réaliser cette première étape :
Vous avez suivi ? Allez, c'est parti pour la prochaine étape !
Étape 2 : Initiez le test en exécutant le code à tester retournant un objet User
Commencez par écrire le test permettant de s'assurer qu'un objet
$user
de typeApp/Entity/User
est bien retourné.Puis créez une classe de test
GithubUserProviderTest
dans le dossierApp/Tests/Security
(rappelez-vous, c'est la préconisation des bonnes pratiques) :
<?php
namespace App\Tests\Security;
use PHPUnit\Framework\TestCase;
class GithubUserProviderTest extends TestCase
{
public function testLoadUserByUsernameReturningAUser()
{
}
{
Dans un premier temps, il faut instancier la classe GithubUserProvider
. Nous avons vu que le constructeur demande deux dépendances. Vous avez le choix entre faire appel aux constructeurs de ces classes ou faire appel à PHPUnit pour créer des doublures.
Doublure ou pas doublure pour la dépendance GuzzleHttp\Client ?
La dépendance GuzzleHttp\Client
est utilisée dans le code pour envoyer une requête à GitHub. Cette requête va récupérer des informations de l'utilisateur qui possède l'access token passé en paramètre de la méthode loadUserByUsername
(via la variable $username
).
Si vous vous souvenez de ce que je vous expliquais au chapitre précédent, vous connaissez la méthode à adopter. Il est préférable de ne pas dépendre de GitHub lors du lancement de nos tests unitaires, donc il faut doubler cet objet afin de maîtriser ce qu'il nous retournera lors de la requête HTTP GET.
Doublure ou pas doublure pour la dépendance JMS\Serializer\Serializer ?
Lorsque l'on lit le constructeur de la classe Serializer, on peut constater qu'il va être bien difficile de l'instancier nous-mêmes.
La meilleure solution, c'est donc que vous le doubliez aussi ! À l'appel de la méthode deserialize
, il faudra vous assurer que vous récupérez bien un tableau avec toutes les informations d'un utilisateur.
Il est temps de créer les doublures des dépendances nécessaires à l'instanciation de la classe GithubUserProvider
, puis d'appeler la méthode à tester. C’est parti :
<?php
namespace App\Tests\Security;
use App\Entity\User;
use App\Security\GithubUserProvider;
use PHPUnit\Framework\TestCase;
class GithubUserProviderTest extends TestCase
{
public function testLoadUserByUsernameReturningAUser()
{
$client = $this->getMockBuilder('GuzzleHttp\Client')
->disableOriginalConstructor()
->getMock();
$serializer = $this
->getMockBuilder('JMS\Serializer\Serializer')
->disableOriginalConstructor()
->getMock();
$githubUserProvider = new GithubUserProvider($client, $serializer);
$user = $githubUserProvider->loadUserByUsername('an-access-token');
}
}
Les doublures permettent de remplir le contrat nécessaire à la création de votre GithubUserProvider
.
Quelques remarques quant au code ci-dessus :
via la méthode
disableOriginalConstructor()
, vous vous assurez que PHPUnit ne fait pas appel au constructeur des classes que vous cherchez à doubler ;vous passez les doublures au constructeur de la classe que vous allez utiliser pour l'exécution du code à tester ;
peu importe ce que vous passez comme paramètre à la méthode
loadUserByUsername
(il s'agit de l'access token communiqué à GitHub). Étant donné que vous ne ferez pas appel à GitHub, cela n'a pas d'incidence ;vous récupérez le résultat de la méthode
loadUserByUsername
. En effet, c'est sur le contenu de la variable$user
que vous ferez vos assertions.
Dans un deuxième temps, il va falloir faire un stub pour le client, en indiquant ce que le retour de la méthode get
devra retourner pour que la variable ne soit pas nulle. Il faut doubler l'objet que nous sommes censés récupérer grâce à la méthode get de l'objet $client
.
<?php
namespace Tests\AppBundle\Security;
use AppBundle\Entity\User;
use AppBundle\Security\GithubUserProvider;
use PHPUnit\Framework\TestCase;
class GithubUserProviderTest extends TestCase
{
public function testLoadUserByUsernameReturningAUser()
{
$client = $this->getMockBuilder('GuzzleHttp\Client')
->disableOriginalConstructor()
->getMock();
$serializer = $this
->getMockBuilder('JMS\Serializer\Serializer')
->disableOriginalConstructor()
->getMock();
$response = $this
->getMockBuilder('Psr\Http\Message\ResponseInterface')
->getMock();
$client->method('get')->willReturn($response);
$githubUserProvider = new GithubUserProvider($client, $serializer);
$user = $githubUserProvider->loadUserByUsername('an-access-token');
}
}
Le but de ce code est de :
créer la doublure pour l'objet
$response
:
$response =
$this->getMockBuilder('Psr\Http\Message\ResponseInterface')
->getMock();
indiquer que la méthode
get
doit retourner l'objet$response
:
$client->method('get')->willReturn($response);
Exécutez le test :
Et… comme vous pouvez le voir, il y a une erreur. Pas de panique ! Nous allons la lire ensemble :
1) App\Tests\Security\GithubUserProviderTest::testLoadUserB yUsernameReturningAUser Error: Call to a member function getContents() on null
PHPUnit vous indique que la méthode getContents
est appelée sur un élément null
. En effet, nous n'avons pas indiqué le comportement que devrait avoir la méthode getBody
.
Allez, c'est parti pour compléter nos tests !
<?php
namespace App\Tests\Security;
use App\Entity\User;
use App\Security\GithubUserProvider;
use PHPUnit\Framework\TestCase;
class GithubUserProviderTest extends TestCase
{
public function testLoadUserByUsernameReturningAUser()
{
// …
$streamedResponse = $this
->getMockBuilder('Psr\Http\Message\StreamInterface')
->getMock();
$response->method('getBody')->willReturn($streamedResponse);
$githubUserProvider = new GithubUserProvider($client, $serializer);
$user = $githubUserProvider->loadUserByUsername('an-access-token');
}
}
Il suffit d'ajouter un double ayant pour type Psr\Http\Message\StreamInterface
, et d'indiquer à PHPUnit que lorsque la méthode getBody
sera exécutée, il faut que ce soit cette doublure qui soit renvoyée.
Lancez à nouveau le test pour vérifier :
Ici nous avons une nouvelle erreur, un peu différente mais qui est de la même nature que la précédente ; voyons voir ce qu’elle raconte :
TypeError: Mock_Serializer_3a7cb633::deserialize(): Argument #1 ($data) must be of type string, null given
L’erreur nous dit que la méthode deserialize attend comme premier argument une chaîne de caractères (string), sauf que l’on lui donne null. Vous avez trouvé ?
Oui voilà, nous devons également ajouter un stub pour notre mock streamedResponse, afin qu’il puisse recevoir sa string. Nous allons faire comme ceci :
$streamedResponse->method('getContents')->willReturn('foo');
Testons à nouveau et regardons maintenant ce que ça donne :
Vous y êtes presque !
Cette fois-ci, vous êtes dans le cas où aucune information n'est contenue dans la variable $userData
après désérialisation.
La solution est de compléter le comportement de la doublure du serializer afin qu'il retourne un tableau avec des informations factices. En avant !
<?php
namespace App\Tests\Security;
use App\Entity\User;
use App\Security\GithubUserProvider;
use PHPUnit\Framework\TestCase;
class GithubUserProviderTest extends TestCase
{
public function testLoadUserByUsernameReturningAUser()
{
// …
$userData = ['login' => 'a login', 'name' => 'user name', 'email' => 'adress@mail.com', 'avatar_url' => 'url to the avatar', 'html_url' => 'url to profile'];
$serializer->method('deserialize')->willReturn($userData);
$githubUserProvider = new GithubUserProvider($client, $serializer);
$user = $githubUserProvider->loadUserByUsername('an-access-token');
}
}
Bien évidemment, vous pouvez mettre n'importe quelles informations dans le tableau $userData
.
Lancez à nouveau le test…
Pas de panique non plus dans ce cas-là, le terme “risky” signifie que c’est un test qui n’effectue pas d’assertion (comme ce qui est écrit dans le message sous la méthode).
Nous pouvons activer une option
( beStrictAboutTestsThatDoNotTestAnything="false"
) dans le fichier de configuration de PHPUnit, qui, placée dans la balise “phpunit” à la suite des autres, désactivera ces warnings.
Faisons l’essai et voyons ce que ça donne :
Ouf, Ça y est !
Allez, voyons tout cela en vidéo pour bien comprendre ce que l’on a fait :
Maintenant que tous nos tests fonctionnent, il faut y ajouter ce qu’on attend d’eux en leur attribuant des attentes (expects).
Néanmoins, pour l'instant, vous ne testez rien. Pour y remédier, vous pouvez commencer par ajouter des attentes (expectations) pour chacun des stubs créés :
<?php
namespaceApp\Tests\Security;
use App\Entity\User;
use App\Security\GithubUserProvider;
use PHPUnit\Framework\TestCase;
class GithubUserProviderTest extends TestCase
{
public function testLoadUserByUsernameReturningAUser()
{
$client = $this->getMockBuilder('GuzzleHttp\Client')
->disableOriginalConstructor()
->getMock();
$serializer = $this
->getMockBuilder('JMS\Serializer\Serializer')
->disableOriginalConstructor()
->getMock();
$response = $this
->getMockBuilder('Psr\Http\Message\ResponseInterface')
->getMock();
$client
->expects($this->once()) // Nous nous attendons à ce que la méthode get soit appelée une fois
->method('get')
->willReturn($response)
;
$streamedResponse = $this
->getMockBuilder('Psr\Http\Message\StreamInterface')
->getMock();
$response
->expects($this->once()) // Nous nous attendons à ce que la méthode getBody soit appelée une fois
->method('getBody')
->willReturn($streamedResponse);
$streamedResponse
->expects($this->once())
->method('getContents')
->willReturn('foo');
$userData = ['login' => 'a login', 'name' => 'user name', 'email' => 'adress@mail.com', 'avatar_url' => 'url to the avatar', 'html_url' => 'url to profile'];
$serializer
->expects($this->once()) // Nous nous attendons à ce que la méthode deserialize soit appelée une fois
->method('deserialize')
->willReturn($userData);
$githubUserProvider = new GithubUserProvider($client, $serializer);
$user = $githubUserProvider->loadUserByUsername('an-access-token');
}
}
En lançant le test, vous aurez logiquement quatre nouvelles assertions :
Assurez-vous maintenant que l'objet retourné par la méthode testée contient bien toutes les informations attendues :
<?php
namespace App\Tests\Security;
use App\Entity\User;
use App\Security\GithubUserProvider;
use PHPUnit\Framework\TestCase;
class GithubUserProviderTest extends TestCase
{
public function testLoadUserByUsernameReturningAUser()
{
// …
$githubUserProvider = new GithubUserProvider($client, $serializer);
$user = $githubUserProvider->loadUserByUsername('an-access-token');
$expectedUser = new User($userData['login'], $userData['name'], $userData['email'], $userData['avatar_url'], $userData['html_url']);
$this->assertEquals($expectedUser, $user);
$this->assertEquals('App\Entity\User', get_class($user));
}
}
Les deux assertions s'assurent que les informations contenues dans l'objet retourné par la méthode loadUserByUsername
sont correctes, et que le type de l'objet est bien App\Entity\User
.
Vous préférez avoir un récapitulatif en vidéo ? Aucun problème :
Alors comment avez-vous vécu cette étape 2 du TP ? N’hésitez pas à la refaire pour vous assurer de bien comprendre la démarche à suivre.
En résumé
Un mock est null de base, il ne faut pas oublier de lui donner des attentes.
Il faut penser à mocker chaque objet en désactivant le constructeur original.
Allons maintenant un peu plus loin ! Dans le prochain chapitre, nous allons voir comment écrire le test couvrant le cas où aucune information ne serait récupérée après la requête HTTP à GitHub, et comment factoriser le code. C’est parti !