• 8 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 12/05/2022

Testez une classe contenant de nombreuses dépendances

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

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 :

  1. Déterminer ce qu’il faut tester.

  2. Initier le test et exécuter le code à tester retournant un objet  User  .

  3. Écrire le test couvrant le cas où aucune information ne serait récupérée après la requête HTTP à GitHub.

  4. 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 :

  1. Une instance de   GuzzleHttp\Client  .

  2. 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 :

  1. Soit retourner un objet   $user  de type   AppBundle\Entity\User  .

  2. 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 type  App/Entity/User  est bien retourné.

  • Puis créez une classe de test  GithubUserProviderTest  dans le dossier  App/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 :

Lancement du 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 :

Lancement du test

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…

Plus d'erreur dans le test mais un terme, risky, qui vient semer le trouble

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 :

Trois nouvelles assertions avec les attentes (expectations)
Trois nouvelles assertions avec les attentes (expectations)

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 !

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