Les tests unitaires (seconde partie)

J’ai déjà fait un article sur les tests unitaires, mais il s’agissait davantage d’une introduction. A présent, j’aimerais entrer dans le vif du sujet en proposant des cas concrets de tests unitaires avec PHPUnit. Ils seront liés à Symfony2/3 (entités, services, formulaire)

Des tests simples

Commençons par une entité simple

Ici, notre entité Media a des votes. On peut imaginer un système de vidéos, d’images, etc. pour lesquelles les internautes pourront voter. Notre premier test, simple, va porter sur la méthode getNewAverageAfterVoteIfMediaHasVotes.
Le but va être de tester deux choses:

  • si le média a déjà des votes, la moyenne des notes est correcte
  • si le média n’a aucun vote, la moyenne des notes est égale à 0

 

Notre premier fichier de test:

Je suis donc dans le répertoire tests à la racine  du site. Ma classe de test étend PHPUnit_Framework_TestCase.

Je commence par créer une entité Media, trois entités votes, et je rattache les votes au média par la méthode addVote. Pour chaque vote, je lui donne bien sûr un score. Enfin, j’appelle la méthode getNewAverageAfterVote() sur le media, qui va me recalculer l’average.

Puis arrive l’assertion qui vérifie qui la moyenne correspond bien à ce qui est retourné par $media->getAverage().

Pour tester, je lance la commande: vendor/bin/phpunit tests/AppBundle/Entity/MediaTest.php

Ensuite, je vais tester la même chose, mais sans votes:

 Le test de la méthode getDisplayedAverage avec un dataProvider

On avait déjà parlé des dataProviders lors du premier article, mais ils vont être redéfinis ici. Le but d’un dataProvider est de rejouer le même test en se basant sur une flopée de données de test, au lieu d’écrire un test par jeu de données.

Ici, de manière très simple, je crée un média et je lui set son average avec le paramètre $actual. Puis je vérifie que le paramètre $expected est égal à ce que retourne $media->getDisplayedAverage(). Ici, $actual et $expected vont prendre successivement les données 5 et ‘5.0’, puis 5.5 et ‘5.5’, 16/3 et ‘5.3’ et enfin null et ‘-‘.

Je ne dois pas oublier de déclarer le dataProvider au dessus de la fonction de test par l’annotation @dataprovider.

Sans ce dataProvider, j’aurais dû écrire 4 tests:

C’est plus lourd, n’est-ce pas?

 Le test de la méthode hasUserAlreadyVoted

Si un utilisateur a déjà voté, je désire que l’appel à la méthode hasUserAlreadyVoted sur un média, avec un user en paramètre, me retourne true, false sinon. Je vais donc créer un média, trois users, trois votes associés à deux de ces users, et enfin rattacher ces votes à média:

Ici donc, les users 1, 2 et 3 ont voté pour le média, mais pas le user4. Je teste donc assertTrue pour les trois premiers users, et assertFalse pour le dernier.

Des tests plus complexes, avec des mocks et des stubs

Mocks et stubs

Sous ces deux noms très barbares se trouvent des notions très simples. Les stubs sont des objets qui implémentent les mêmes méthodes que l’objet réel. Ces méthodes ne font rien et sont configurées pour retourner une valeur spécifique.

Les mocks sont des stubs capables de surcroît de tracer leurs appels (on spécifie uniquement les méthodes qui seront appelées) et de vérifier certaines conditions de ces appels (les exceptions par exemple).

Sur ce site, on peut lire:

  • Stub est centré sur le système testé (SUT). Il lui fourni une « indépendance » sous forme d’un objet véhiculant certaines données qui seront utilisées par le SUT pendant le test. Le stub ne peut jamais faire échouer le test. L’assertion est effectuée directement contre le SUT. Ceci s’appelle « state-based testing » car on vérifie juste l’état de notre SUT à la fin du test et non la manière dont cet état a été obtenu.
  • Mock est centré sur le test. Lors de la création du mock nous enregistrons un certain nombre d’attentes qui sont vérifiées pendant l’exécution du test. Le mock décide ci le test échoue ou réussi donc notre assertion nous la faisons contre le mock et non contre le SUT comme c’était dans le cas du stub. Dans le cas du mock ce n’est pas le résultat final qui nous intéresse mais la manière dont il a été obtenu ; Combien de fois une méthode a été invoquée ? Un évènement a été levé ? Qu’est-ce qu’il a été fait dans cette évènement ?, etc. Nous appelons ce cas « Interactions testing ».

Vous trouverez des exemples dans une présentation que j’avais faite: http://slides.com/jeanpierresaulnier/tests-unitaires

La majorité du temps, ce sont des mocks que vous utiliserez, pour simuler une classe, indiquer combien de fois vous désirez qu’une méthode soit appelée, ce que vous attendez comme donnée simulée pour cette méthode.

Parfois, juste parce que vous aurez besoin d’une dépendance dans un constructeur, vous utilisez simplement un stub. Vous allez rapidement comprendre par l’exemple.

Le test d’un service VoteManager

J’ai un VoteManager qui est très simple. Il prend en dépendance le voteRepository et le tokenStorage, va permettre de créer un nouveau vote en fonction de l’utilisateur connecté, et également de sauver un vote.

Comme nous avons des dépendances, et que nous n’allons pas véritablement faire un appel en bdd, ou chercher un utilisateur connecté (ça, ça serait pour un test fonctionnel), il va falloir simuler tous ces comportements (on emploie généralement le terme mocker, même si on créé un stub, ce qui peut prêter à confusion.)

 

Créons nos premiers stubs

Ici, j’ai remis les dépendances que j’avais dans le VoteManager, à savoir le voteRepository et le tokenStorage. J’en ai rajouté deux autres: le token et le voteManager. Pourquoi? Car à un moment donné, le tokenStorage va avoir besoin de faire appel à la méthode getToken() et devra retourner un token. Sauf que l’on ne veut pas retourner un véritable token (d’une classe étendant AbstractToken et implémentant l’interface TokenInterface), mais seulement une simulation, un stub de ce token.

Analysons les quatre stubs:

J’ai simplement créé un stub qui va me renvoyer un token simulé, et j’utilise son interface pour cela:

 

Je fais de même avec le tokenStorage. Je dois juste le passer dans un constructeur lors de la création du voteManager, je n’ai pas besoin qu’il fasse quelque chose de particulier pour le moment:

 

Ensuite, pour le voteRepository, je suis allé un petit peu plus loin. J’ai fait un stub partiel, c’est à dire que je ne vais simuler que la méthode save (puisque je ne vais pas vraiment sauver en bdd). Je n’ai pas besoin d’arguments dans le constructeur, donc je mets []. Je n’ai pas besoin de définir un mockClassName non plus, je mets donc «  ». Et enfin, je veux désactiver le constructeur, donc je mets false en 5ème argument:

 

Enfin, il y a le voteManager. De même, comme c’est lui que je vais tester, mais que je ne peux faire appel à sa méthode save, je vais faire un stub partiel et dire que je ne vais simuler que la méthode save. J’ai des arguments dans le constructeur, que je désire prendre en compte, et les indique dans en troisième paramètre:

 

Le premier test et nos premiers mocks

Nous rentrons ici dans le vif du sujet. Nous allons transformer les stubs token et tokenStorage en mock avec la méthode expects.

Ici, je créer un user, un média, un vote, et je lis le user et le média au vote. Rappelez-vous ce que fait la méthode getNewVote du VoteManager:

 

Je dois donc d’abord mocker la méthode getToken du tokenStorage en disant qu’elle sera appelée une fois et retournera une classe implémentant TokenInterface:

C’est parfait. Mais voilà, une fois que l’on a appelé la méthode getToken(), on voit que l’on appelle ensuite la méthode getUser(). Il faut aussi mocker ça en disant que nous allons appeler une fois cette méthode et retourner un user:

 

Bien, une fois tout ceci fait, nous pouvons facile vérifier que le vote retourné par la méthode getNewVote($media) est bien un objet ayant les mêmes valeurs que celui que nous avons créé au début du test:

en faisant appel à cette assertion:

Un second test avec des mocks

Nous allons tester la méthode saveVote():

 

Nous allons donc mocker la méthode save du voteRepository. Par contre, ici, nous voulons vérifier aussi que $media va appeler une fois la méthode addVote(), et nous allons donc devoir la mocker.

Tester les violations

Imaginons que j’ai une entité appelée Contact, qui va me servir pour le formulaire de contact. Disons que j’ai une liste correspondant au champ knowledge. Ce champs sera nourri par une liste déroulante de réponses à la question « Comment nous avez-vous connu? ». Parfait. Mais nous voulons aussi permettre de cocher « autre », et dans ce cas là, un champs texte va apparaitre (je vais appeler le champs « other »):

Ce que je veux, c’est que si l’internaute coche « autre » et qu’il ne remplit pas ce champs texte, une violation soit lancée sur le champs other. Nous allons donc tester cela:

Un premier test avec violation

Tout d’abord, nous nous disons, en voyant la méthode validate qu’il faut mocker ExecutionContextInterface. Seul petit problème, la fonction buildViolation va renvoyer une ConstraintViolationBuilderInterface qui appellera atPath et addViolation. Il faut donc aussi mocker ceci.

Du coup, on commence par mocker une ConstraintViolationBuilderInterface:

Et l’on va dire que l’on va mocker dans l’ordre les appels atPath et buildViolation:

 

Enfin, nous pouvons mocker l’ExecutionContextInterface qui va appeler buildViolation et retourner une ConstraintViolationInterface, et enfin faire appeler à la méthode validate:

 

 Un second test sans violation

Identique au premier test, mais on vérifie qu’on n’appelle jamais la plupart des méthodes.

 

Test d’un mail envoyé

 

J’ai donc un ContactManager qui va se charger d’envoyer le mail. La première chose à faire est de mocker les dépendances:

 

Ensuite, on teste l’envoi du mail et on vérifie que le template appelé est le bon, avec les appels corrects de méthodes:

 

Rédigé par

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *