Utiliser des Mock avec PHPUnit
Par Clochix le dimanche 27 septembre 2009, 15:21 - Technoweb - Lien permanent
Les mocks et les stubs (on utilise le terme de "bouchons" en français) sont des objets utilisés dans le cadre de tests unitaires, afin de simuler le fonctionnement d'objets dont dépendent les composants que l'on teste. Le billet d'Olivier "PHPPRO" sur leur utilisation m'a rappelé que j'avais commencé à prendre des notes sur l'implémentation des bouchons dans PHPUnit. Je n'ai malheureusement jamais terminé ce billet, et n'ai pas trop l'envie en ce moment de passer quelques heures à le fignoler, je me permet donc de vous livrer mes notes, en espérant qu'elles fournissent au moins des pistes pour aller creuser tout ça
Les tests unitaires devraient, comme leur nom l'indique, porter sur le niveau de code le plus fin, tester chaque portion indépendamment du contexte. Il faut donc pour cela essayer de rendre le code le plus indépendant possible de ce contexte, pour pouvoir isoler chacune de ses composantes afin que le résultat des tests ne soit fonction d'aucun facteur externe non contrôlé.
Une pratique courante pour tester du code avec des dépendances est donc de remplacer celles-ci par des "bouchons" (stubs), des objets au code le plus simple possible, pour minimiser les risques d'erreur, et dont on contrôle complètement le fonctionnement.
Par exemple, si notre code fait appel à une base de données, on remplacera l'objet encapsulant les appels à celle-ci par un objet qui retournera exactement les résultats que l'on attendra. Ainsi on pourra exécuter nos tests sans se soucier de la présence et de la disponibilité d'une vrai base, et étudier le comportement du code face à des erreurs ou dans les cas limites, par exemple lorsque la base est indisponible.
Pour être plus précis, on utilise le terme bouchon pour désigner deux concepts assez proches:
- les stubs sont des objets ou des méthodes bidons qui se contentent de renvoyer un résultat. Ce résultat peut être constant ou calculé;
- les mocks sont des stubs capable de surcroît de tracer leurs appels et de vérifier certaines conditions de ces appels;
Pour utiliser des bouchons avec PHPUnit, on utilisera la méthode
getMock de PHPUnit_Framework_TestCase. Celle-ci
attend au moins un paramètre, le nom de la classe à simuler. Elle retourne un
MockObject.
Un petit exemple sera sans doute plus parlant. Si l'on veut simuler un objet
d'une classe Toto dont la méthode titi doit renvoyer
42, on pourra écrire:
<?php
require_once 'PHPUnit/Framework.php';
class StubTest extends PHPUnit_Framework_TestCase {
public function testStub () {
$stub = $this->getMock('Toto');
$stub->expects($this->any())
->method('titi')
->will($this->returnValue(42));
}
?>
La valeur de retour de la méthode pourra soit être fixée, comme ci-dessus, soit être un des arguments d'appel, soit faire appel à une fonction ou une méthode via un callback, ou enfin lever une exception:
->will($this->returnArgument(0));
->will($this->returnCallback('FonctionDeCallback'));
->will($this->returnCallback(array('Classe', 'Méthode'));
->will($this->throwException(new Exception()));
Les Mocks
Je le disais plus haut, les mocks permettent aussi de demander au framework de test d'effectuer des contrôles sur l'utilisation d'un bouchon dans le cadre d'un test particuliers. Si l'on veut s'assurer qu'une méthode est appelée trois fois, il suffit d'écrire:
$stub->expects($this->exactly(3))->method('titi')->will($this->returnValue(42));
PHPUnit lèvera automatiquement une erreur si à l'issue du test la méthode n'a pas été appelée trois fois.
La méthode expect permet de définir un comportement différent
selon les appels. Par exemple, si une méthode doit renvoyer vrai lors
de son premier appel et faux pour les suivants:
$stub = $this->getMock('MyClass');
$stub->expects($this->at(0))->method('test')->will($this->returnValue(true));
$stub->expects($this->any())->method('test')->will($this->returnValue(false));
On peut aussi utiliser onConsecutiveCalls() pour renvoyer des
résultats différents à chaque appel:
->will($this->onConsecutiveCalls('res1', 'res2', 'res3'));
On peut utiliser expect avec les méthodes suivantes:
any();never()pour s'assurer qu'une méthode n'est jamais appelée;atLeastOnce();once(): appelée une seule fois;exactly($count): appelée un nombre exact de fois;at($index): comportement lors du nième appel;
Contrôler les paramètres
On peut également tester les paramètres d'appel. PHPUnit déclenchera une
erreur si la méthode titi est appelée avec un premier paramètre
différent de tata, ou un deuxième qui n'est pas un tableau, ou un
troisième qui est nul:
$stub = $this->getMock('toto', array('titi'));
$stub->expects($this->any())->method('titi')
->with(
$this->equalTo('tata'),
$this->isType(PHPUnit_Framework_Constraint_IsType::TYPE_ARRAY),
$this->logicalNot($this->isNull())
);
Ce sont les bases pour commencer à jouer. En lisant le code de PHPUnit, on découvre de nombreuses autres fonctions plus avancées, malheureusement non décrites dans la documentation et dont il est difficile de trouver des exemples. Si vous utilisez d'autres fonctions, n'hésitez pas à les signaler ici.
Commentaires
Merci pour ce retour sur l'utilisation des fonctionnalités Mock de PHPUnit.
Pour ma part, j'évite de l'utiliser car je souhaite rester indépendant d'un quelconque framework lors de mon développement.
Les fonctionnalités Mock de PHPUnit sont très intéressantes, elles ont cependant de mon point de vue un défaut majeure dans l'utilisation que je fais des mocks. En effet, lorsque je développe j'utilise la version "mock" de mes classes (adapter) dans mon code tant que je n'ai pas développé la version native (standard). Cela me permet de prototyper / présenter rapidement les fonctionnalités "statiques" de mes développements et de "débrancher" lorsque je suis prêt le mock pour mettre la vraie implémentation (imaginez que je développe un webservice et que je doive rapidement fournir une version de mon webservice pour des consommateurs qui seraient "impatients"). Le problème avec le framework PHPUnit est qu'il n'est pas adapté a priori pour une utilisation en dehors des tests unitaires.
autre part les mocks peuvent être utiles dans d'autres cas encore que les tests unitaires et celui que je décris, par exemple certains tests d'intégration qui porte sur d'autres zones du code... même remarque dans ce cas.
Et vous quels sont vos pratiques pour mettre en place des bouchons dans les cas autres que les tests unitaires ?
@Olivier : en fait on ne parle pas tout à fait de la même chose. Ce que je décris ici n'est valable évidemment que dans le contexte de tests unitaires, je ne l'utiliserais pas pour créer des bouchons applicatifs, ne serait-ce que parce qu'il est rare que PHPUnit soit présent ailleurs que dans les environnements de développement.
Je pense qu'il est utile de connaître l'existence de ces fonctions. Ensuite il faut appliquer à chaque cas particuliers la réponse la plus appropriée. Pour les tests, je peux utiliser les Mock de PHPUnit, ou créer des bouchons spécifiques, qui le plus souvent sont des proxy entre le test et le code métier.
Dans les autres cas, je crée généralement les classes dont j'ai besoin, les méthodes renvoyant des résultats "en dur" que je remplace au fur et à mesure par le vrai code. Je n'ai pas encore eu l'occasion de devoir utiliser des Mock plus de quelques jours avant de disposer du code final.