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.