5. Comment écrire des scénarios de test

Après avoir créer votre premier test et compris comment le lancer<lancement-des-tests>, vous voulez probablement écrire de meilleurs tests. Dans cette section, vous trouverez comment ajouter du sucre syntaxique pour vous aider dans l’écriture de vos tests et ce de manière simple.

Il est possible d’écrire des tests unitaires avec atoum de plusieurs manières, et l’une d’elles est d’utiliser des mots-clefs tels que given, if, and ou bien encore then, when ou assert qui permettent de mieux organiser et de rendre plus lisibles les tests.

5.1. given, if, and et then

L’utilisation de ces mots-clefs est très intuitive :

<?php
$this
    ->given($computer = new computer())
    ->if($computer->prepare())
    ->and(
        $computer->setFirstOperand(2),
        $computer->setSecondOperand(2)
    )
    ->then
        ->object($computer->add())
            ->isIdenticalTo($computer)
        ->integer($computer->getResult())
            ->isEqualTo(4)
;

Il est important de noter que ces mots-clés n’ont pas un autre but que de donner aux tests une forme plus lisible. Il ne serve aucun but technique. Le seul but est d’aider le lecteur, les humains ou plus précisément le développeur, à comprendre ce qui se passe dans le test.

Ainsi, given, if et and permettent de définir les conditions préalables pour que les assertions qui suivent le mot-clef then passent avec succès.

Cependant, il n’y a aucune règle ou grammaire qui régissent la syntaxe de l’ordre de ces mots-clés pour atoum.

Ainsi, le développeur utilisera ces mots-clés à bon escient pour rendre les tests aussi lisibles que possible. Cependant, si elle est utilisé de façon incorrecte, vous pourriez finir avec des tests comme suit :

<?php
$this
    ->and($computer = new computer())
    ->if($computer->setFirstOperand(2))
    ->then
    ->given($computer->setSecondOperand(2))
        ->object($computer->add())
            ->isIdenticalTo($computer)
        ->integer($computer->getResult())
            ->isEqualTo(4)
;

Pour les mêmes raisons, l’utilisation de then est facultative.

Il est également important de noter qu’il est tout à fait possible d’écrire le même test en n’utilisant aucun mot-clef :

<?php
$computer = new computer();
$computer->setFirstOperand(2);
$computer->setSecondOperand(2);

$this
    ->object($computer->add())
        ->isIdenticalTo($computer)
    ->integer($computer->getResult())
        ->isEqualTo(4)
;

Le test ne sera pas plus lent ou plus rapide à exécuter, et il n’y a aucun avantage à utiliser une notation plutôt qu’une autre, l’important est d’en choisir une et de s’y tenir. De cette façon, cela permet de facilité la maintenance des tests (le problème est exactement le même que les conventions de codage).

5.2. when

En plus de given, if, and et then, il existe également d’autres mots-clefs.

L’un d’entre eux est when. Il dispose d’une fonctionnalité spécifique introduite pour contourner le fait qu’il est illégal d’écrire en PHP le code suivant :

<?php # ignore
$this
    ->if($array = array(uniqid()))
    ->and(unset($array[0]))
    ->then
        ->sizeOf($array)
            ->isZero()
;

Le langage génère en effet dans ce cas l’erreur fatale : Parse error: syntax error, unexpected 'unset' (T_UNSET), expecting ')'

Il est en effet impossible d’utiliser unset() comme argument d’une fonction.

Pour résoudre ce problème, le mot-clef when est capable d’interpréter l’éventuelle fonction anonyme qui lui est passée en argument, ce qui permet d’écrire le test précédent de la manière suivante :

<?php
$this
    ->if($array = array(uniqid()))
    ->when(
        function() use ($array) {
            unset($array[0]);
        }
    )
    ->then
      ->sizeOf($array)
        ->isZero()
;

Bien évidemment, si when ne reçoit pas de fonction anonyme en argument, il se comporte exactement comme given, if, and et then, à savoir qu’il ne fait absolument rien fonctionnellement parlant.

5.3. assert

Enfin, il existe le mot-clef assert qui a également un fonctionnement un peu particulier.

Pour illustrer son fonctionnement, le test suivant va être utilisé :

<?php
$this
    ->given($foo = new \mock\foo())
    ->and($bar = new bar($foo))
    ->if($bar->doSomething())
    ->then
        ->mock($foo)
            ->call('doOtherThing')
                ->once()

    ->if($bar->setValue(uniqid()))
    ->then
        ->mock($foo)
            ->call('doOtherThing')
                ->exactly(2)
;

Le test précédent présente un inconvénient en terme de maintenance, car si le développeur a besoin d’intercaler un ou plusieurs nouveaux appels à bar::doOtherThing() entre les deux appels déjà effectués, il sera obligé de mettre à jour en conséquence la valeur de l’argument passé à exactly(). Pour remédier à ce problème, vous pouvez remettre à zéro un mock de 2 manières différentes :

  • soit en utilisant $mock->getMockController()->resetCalls() ;
  • soit en utilisant $this->resetMock($mock).
<?php
$this
    ->given($foo = new \mock\foo())
    ->and($bar = new bar($foo))
    ->if($bar->doSomething())
    ->then
        ->mock($foo)
            ->call('doOtherThing')
                ->once()

    // 1ère manière
    ->given($foo->getMockController()->resetCalls())
    ->if($bar->setValue(uniqid()))
    ->then
        ->mock($foo)
            ->call('doOtherThing')
                ->once()

    // 2ème manière
    ->given($this->resetMock($foo))
    ->if($bar->setValue(uniqid()))
    ->then
        ->mock($foo)
            ->call('doOtherThing')
                ->once()
;

Ces méthodes effacent la mémoire du contrôleur, il est donc possible d’écrire l’assertion suivante comme si le bouchon n’avait jamais été utilisé.

Le mot-clef assert permet de se passer de l’appel explicite à resetCalls() ou resetMock et de plus il provoque l’effacement de la mémoire de l’ensemble des adaptateurs et des contrôleurs de mock définis au moment de son utilisation.

Grâce à lui, il est donc possible d’écrire le test précédent d’une façon plus simple et plus lisible, d’autant qu’il est possible de passer une chaîne de caractère à assert afin d’expliquer le rôle des assertions suivantes :

<?php
$this
    ->assert("Bar n'a pas de valeur")
        ->given($foo = new \mock\foo())
        ->and($bar = new bar($foo))
        ->if($bar->doSomething())
        ->then
            ->mock($foo)
                ->call('doOtherThing')
                    ->once()

    ->assert('Bar a une valeur')
        ->if($bar->setValue(uniqid()))
        ->then
            ->mock($foo)
                ->call('doOtherThing')
                    ->once()
;

La chaîne de caractères sera de plus reprise dans les messages générés par atoum si l’une des assertions ne passe pas avec succès.

5.4. newTestedInstance & testedInstance

Lorsque l’on effectue des tests, il faut bien souvent créer une nouvelle instance de la classe et passer celle-ci dans divers paramètres. Une aide à l’écriture est disponible pour ce cas précis, il s’agit de newTestedInstance et de testedInstance

Voici un exemple :

namespace jubianchi\atoum\preview\tests\units;

use atoum;
use jubianchi\atoum\preview\foo as testedClass;

class foo extends atoum
{
    public function testBar()
    {
        $this
            ->if($foo = new testedClass())
            ->then
                ->object($foo->bar())->isIdenticalTo($foo)
        ;
    }
}

Ceci peut être simplifié avec la nouvelle syntaxe :

namespace jubianchi\atoum\preview\tests\units;

use atoum;

class foo extends atoum
{
    public function testBar()
    {
        $this
            ->if($this->newTestedInstance)
            ->then
                ->object($this->testedInstance->bar())
                    ->isTestedInstance()
        ;
    }
}

Comme on le voit, c’est légèrement plus simple, mais surtout cela présente deux avantages :

  • On ne manipule pas le nom de la classe testée
  • On ne manipule pas l’instance ainsi créée

Par ailleurs, on peut facilement valider que l’on a bien l’instance testée avec isTestedInstance, comme expliqué dans l’exemple précédent.

Pour passer des arguments au constructeur, il suffit de le faire au travers de newTestedInstance :

$this->newTestedInstance($argument1, $argument2);

Si vous voulez tester une méthode statique de votre classe, vous pouvez récupérer la classe testée avec cette syntaxe :

namespace jubianchi\atoum\preview\tests\units;

use atoum;

class foo extends atoum
{
    public function testBar()
    {
      $this
        ->if($class = $this->testedClass->getClass())
        ->then
          ->object($class::bar())
       ;
    }
 }

5.4.1. Accès aux constantes de la classe testée

Si vous avez besoin d’accéder aux constantes de la classe testée, vous pouvez y accéder de deux façons :

<?php

namespace
{
    class Foo
    {
        const A = 'a';
    }
}

namespace tests\units
{
    class Foo extends \atoum\test
    {
        public function testFoo()
        {
            $this
                ->given($this->newTestedInstance())
                ->then
                    ->string($this->getTestedClassName()::A)->isEqualTo('a')
                    ->string($this->testedInstance::A)->isEqualTo('a')
            ;
        }
    }
}

Avertissement

Vous avez besoin d’initialiser l’instance avec newTestedInstance, pour avoir accès aux constantes.

5.5. testedClass

Comme testedInstance, vous pouvez utiliser testedClass pour écrire des tests plus compréhensible. testedClass permet d’écrire des assertions dynamiques sur les classes testées :

<?php
$this
        ->testedClass
    ->hasConstant('FOO')
                ->isFinal()
;

Vous pouvez aller plus loin avec les assertions de classe.