Prendre le contrôle des tests

Pour tester un module de code vous avez besoin d'avoir un contrôle très précis sur son environnement. Si quelque chose change dans les coulisses, par exemple dans un fichier de configuration, alors les tests peuvent échouer de façon inattendue. Il ne s'agirait plus d'un test de code sans équivoque et pourrait vous faire perdre des heures précieuses à la recherche d'erreurs dans un code qui fonctionne. Alors qu'il s'agit d'un problème de configuration qui plante le test en question. Au mieux vos scénarios de test deviennent de plus en plus compliqués afin de prendre en compte toutes les variations possibles.

Contrôler le temps

Il y a souvent beaucoup de variables évidentes qui peuvent affecter un scénario de test unitaire, d'autant plus dans un environnement de développement web dans lequel PHP a ses aises. Parmi celles-ci, on trouve les paramètres de connexion à la base de données et ceux de configuration, les droits de fichier et les ressources réseau, etc. L'échec ou la mauvaise installation de l'un ou l'autre de ces composants cassera la suite de test.

Est-ce que nous devons ajouter des tests pour valider l'installation de ces composants ? C'est une bonne idée mais si vous les placez dans les tests du module de code vous aller commencer à encombrer votre code de test avec des détails hors de propos avec la tâche en cours. Ils doivent être placés dans leur propre suite de tests.

Par contre un autre problème reste : nos machines de développement doivent aussi avoir tous les composants système d'installés avant l'exécution de la suite de test. Et vos tests s'exécuteront plus lentement.

Devant un tel dilemme, nous créerons souvent des versions enveloppantes des classes qui gèrent ces ressources. Les vilains détails de ces ressources sont ensuite codés une seule fois. J'aime bien appeler ces classes des "classes passerelle" étant donné qu'elles existent en bordure de l'application, l'interface entre votre application et le reste du système. Ces classes passerelle sont - dans le meilleur des cas - simulées pendant les tests par des versions de simulacre. Elles s'exécutent plus rapidement et sont souvent appelées "bouchon serveur [Ndt : Server Stubs]" ou dans leur forme plus générique "objet fantaisie [Ndt : Mock Objects]". Envelopper et bouchonner chacune de ces ressources permet d'économiser pas mal de temps.

Un des facteurs souvent négligés reste le temps.

Par exemple, pour tester l'expiration d'une session des codeurs vont souvent temporairement en caler la durée à une valeur très courte, disons 2 secondes, et ensuite effectuer un sleep(3) : ils estiment alors que la session a expirée. Sauf que cette opération ajoute 3 secondes à la suite de test : il s'agit souvent de beaucoup de code en plus pour rendre la classe de session aussi malléable. Plus simple serait d'avoir un moyen d'avancer l'horloge arbitrairement. De contrôler le temps.

Une classe horloge

Une nouvelle fois, nous allons effectuer notre conception d'une enveloppe d'horloge via l'écriture de tests. Premièrement nous ajoutons un scénario de test d'horloge dans notre suite de test tests/all_tests.php...

<?php
require_once(dirname(__FILE__) . '/simpletest/autorun.php');
require_once(dirname(__FILE__) . '/log_test.php');
require_once(dirname(__FILE__) . '/clock_test.php');

class AllTests extends TestSuite {
    function __construct() {
        parent::__construct();
        $this->addTest(new TestOfLogging());
        $this->addTest(new TestOfClock());
    }
}
?>

Ensuite nous créons le scénario de test dans un nouveau fichier tests/clock_test.php...

<?php
require_once(dirname(__FILE__) . '/../classes/clock.php');

class TestOfClock extends UnitTestCase {
    function testClockTellsTime() {
        $clock = new Clock();
        $this->assertEqual($clock->now(), time());
    }
}
?>

Notre unique test pour le moment, c'est que notre nouvelle class Clock se comporte comme un simple substitut de la fonction time() en PHP. Nous écrirons cette fonctionnalité de décalage dans le temps une fois que nous serons au vert. Pour le moment nous ne sommes évidemment pas dans le vert...


Fatal error: Failed opening required '../classes/clock.php' (include_path='') in /home/marcus/projects/lastcraft/tutorial_tests/tests/clock_test.php on line 2

Si vous ne voyez pas ce genre d'erreurs, c'est probablement que vos paramètres d'erreurs ont besoin d'un petit ajustement. Vous aurez peut-être envie d'ajouter ces quelques lignes en tête de votre fichier de test :

ini_set('display_errors', 1);
error_reporting(E_ALL);

La documentation PHP pourrait devenir pratique si vous êtes bloqué sans voire cette Fatal error.

Considérons que l'erreur s'affiche bien, nous pouvons alors continuer et créer un fichier classes/clock.php...

<?php
class Clock {
    function now() {
    }
}
?>

De la sorte nous reprenons le cours du code.

AllTests

Fail: TestOfClock -> testClockTellsTime -> [NULL: ] should be equal to [integer: 1050257362]
3/3 test cases complete. 4 passes, 1 fails and 0 exceptions.
Facile à corriger...

class Clock {
    function now() {
        return time();
    }
}

Et nous revoici dans le vert...

AllTests

3/3 test cases complete. 5 passes, 0 fails and 0 exceptions.
Il y a juste un petit problème.

L'horloge pourrait basculer pendant l'assertion et créer un écart d'une seconde. Les probabilités sont assez faibles mais s'il devait y avoir beaucoup de tests de chronométrage nous finirions avec une suite de test qui serait erratique et forcément presque inutile. Nous nous y attaquerons bientôt et pour l'instant nous l'ajoutons dans la liste des "choses à faire".

Le test d'avancement ressemble à...

class TestOfClock extends UnitTestCase {

    function testClockTellsTime() {
        $clock = new Clock();
        $this->assertEqual($clock->now(), time());
    }
    
    function testClockAdvance() {
        $clock = new Clock();
        $clock->advance(10);
        $this->assertEqual($clock->now(), time() + 10);
    }
}

Le code pour arriver au vert est direct : il suffit d'ajouter un décalage de temps.

class Clock {
    private $offset = 0;
    
    function now() {
        return time() + $this->offset;
    }
    
    function advance($offset) {
        $this->offset += $offset;
    }
}

Nettoyer la suite de tests

Notre fichier all_tests.php contient des répétitions dont nous pourrions nous débarrasser. Nous devons ajouter manuellement tous nos scénarios de test depuis chaque fichier inclus. C'est possible de les enlever mais avec les précautions suivantes. La classe GroupTest inclue une méthode bien pratique appelée addTestFile() qui prend un fichier PHP comme paramètre. Ce mécanisme prend note de toutes les classes : elle inclut le fichier et ensuite regarde toutes les classes nouvellement créées. S'il y a des filles de SimpleTestCase elles sont ajoutées comme une nouvelle TestSuite.

Voici notre suite de test remaniée en appliquant cette méthode...

<?php
require_once(dirname(__FILE__) . '/simpletest/autorun.php');
    
class AllTests extends TestSuite {
    function AllTests() {
        parent::__construct();
        $this->addFile('log_test.php');
        $this->addFile('clock_test.php');
    }
}
?>

Les inconvéniants sont les suivants...

  1. Si le fichier de test a déjà été inclus, aucune nouvelle classe ne sera ajoutée au groupe.
  2. Si le fichier de test contient d'autres classes reliées à SimpleTestCase alors celles-ci aussi seront ajouté au test de groupe.
In practice neither of these turn out to be problems. Test suites are usually a tree structure, so it's rare to need a test case in two places. En pratique, ni l'un ni l'autre ne sont véritablement un soucis. Les suites de tests sont généralement structurées en arbre, il est donc très rare qu'un test se retrouve dans deux endroits.

Nous devrions corriger au plus vite le petit problème de décalage possible sur l'horloge : c'est ce que nous faisons ensuite.

Le temps est souvent une variable négligée dans les tests.
Une classe horloge nous permet de modifier le temps.
Nettoyer la suite de tests.
La section précédente : grouper des tests unitaires en suite.
La section suivante : sous classer les scénarios de test.
Vous aurez besoin du testeur unitaire SimpleTest pour les exemples.