Sous classer un scénario de test unitaire

Cette page...

Une assertion insensible au chronomètre

Nous avions laissé notre test d'horloge avec un trou. Si la fonction time() de PHP avançait pendant cette comparaison...

function testClockTellsTime() {
    $clock = new Clock();
    $this->assertEqual($clock->now(), time(), 'Now is the right time');
}
...notre test aurait un écart d'une seconde et entraînerait un faux échec. Un comportement erratique de notre suite de test n'est vraiment pas ce que nous souhaitons : nous pourrions la lancer une centaine de fois par jour.

Nous pourrions ré-écrire notre test...

function testClockTellsTime() {
    $clock = new Clock();
    $time1 = $clock->now();
    $time2 = time();
    $this->assertTrue(($time1 == $time2) || ($time1 + 1 == $time2), 'Now is the right time');
}
Sauf que la conception n'est pas plus claire et que nous devrons le répéter pour chaque test de chronométrage. Les répétitions sont un ennemi public n°1 et donc un très bon stimulant pour le remaniement de notre code de test.
class TestOfClock extends UnitTestCase {
    function TestOfClock() {
        $this->UnitTestCase('Clock class test');
    }
    function assertSameTime($time1, $time2, $message) {
        $this->assertTrue(
                ($time1 == $time2) || ($time1 + 1 == $time2),
                $message);
    }
    function testClockTellsTime() {
        $clock = new Clock();
        $this->assertSameTime($clock->now(), time(), 'Now is the right time');
    }
    function testClockAdvance() {
        $clock = new Clock();
        $clock->advance(10);
        $this->assertSameTime($clock->now(), time() + 10, 'Advancement');
    }
}
Bien entendu à chaque modification je relance les tests pour bien vérifier que nous sommes dans les clous. Remaniement au vert. C'est beaucoup plus sûr.

Réutiliser notre assertion

Peut-être voulons nous ajouter d'autres tests sensibles au temps. Peut-être lisons nous des timestamps - en provenance d'une entrée dans une base de données ou d'ailleurs - qui tiendraient compte d'une simple seconde pour basculer. Pour que ces nouvelles classes de test profitent de notre nouvelle assertion nous avons besoin de la placer dans une "super classe".

Voici notre fichier clock_test.php au complet après la promotion de notre méthode assertSameTime() dans sa propre "super classe"...

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

    class TimeTestCase extends UnitTestCase {
        function TimeTestCase($test_name) {
            $this->UnitTestCase($test_name);
        }
        function assertSameTime($time1, $time2, $message) {
            $this->assertTrue(
                    ($time1 == $time2) || ($time1 + 1 == $time2),
                    $message);
        }
    }
    
    class TestOfClock extends TimeTestCase {
        function TestOfClock() {
            $this->TimeTestCase('Clock class test');
        }
        function testClockTellsTime() {
            $clock = new Clock();
            $this->assertSameTime($clock->now(), time(), 'Now is the right time');
        }
        function testClockAdvance() {
            $clock = new Clock();
            $clock->advance(10);
            $this->assertSameTime($clock->now(), time() + 10, 'Advancement');
        }
    }
?>
Désormais nous bénéficions de notre nouvelle assertion à chaque fois que nous héritons de notre propre classe TimeTestCase plutôt que de la classe par défaut UnitTestCase. Nous retrouvons la conception de l'outil JUnit et SimpleTest est un portage en PHP de cette interface. Il s'agit d'un framework de test à partir duquel votre propre système de test peut s'agrandir.

Si nous lançons les tests maintenant une légère broutille survient...

Warning: Missing argument 1 for timetestcase() in /home/marcus/projects/lastcraft/tutorial_tests/tests/clock_test.php on line 5

All tests

3/3 test cases complete. 6 passes and 0 fails.
La raison est assez délicate.

Notre sous-classe exige un paramètre dans le constructeur qui n'a pas été fourni et pourtant il semblerait que nous l'ayons bel et bien fourni. Quand nous avons hérité de notre nouvelle casse nous lui avons passé notre propre constructeur. C'est juste là...

function TestOfClock() {
    $this->TimeTestCase('Clock class test');
}
En fait nous avons raison, là n'est pas le problème.

Vous vous souvenez de quand nous avons construit le test de groupe all_tests.php en utilisant la méthode addTestFile(). Cette méthode recherche les classes de scénario de test, les instancie si elles sont nouvelles puis exécute tous nos tests. Ce qui s'est passé ? Elle a aussi trouvé notre extension de scénario de test. C'est sans conséquence puisque qu'il n'y a pas de méthode de test à l'intérieur - comprendre pas de méthode commençant par "test". Aucun test supplémentaire n'est exécuté.

Le problème vient du fait qu'il instancie la classe et le fait sans le paramètre $test_name qui déclenche l'avertissement. Ce paramètre n'est généralement nécessaire ni pour un scénario de test, ni pour son assertion. Pour que notre scénario de test étendu corresponde à l'interface de UnitTestCase, nous avons besoin de le rendre optionnel...

class TimeTestCase extends UnitTestCase {
    function TimeTestCase($test_name = false) {
        $this->UnitTestCase($test_name);
    }
    function assertSameTime($time1, $time2, $message = false) {
        if (! $message) {
            $message = "Time [$time1] should match time [$time2]";
        }
        $this->assertTrue(
                ($time1 == $time2) || ($time1 + 1 == $time2),
                $message);
    }
}
Bien sûr, que cette classe soit instanciée sans raison par la suite de test devrait continuer à vous ennuyer. Voici une modification pour l'empêcher de s'exécuter...
SimpleTestOptions::ignore('TimeTestCase');
class TimeTestCase extends UnitTestCase {
    function TimeTestCase($test_name = false) {
        $this->UnitTestCase($test_name);
    }
    function assertSameTime($time1, $time2, $message = '') {
        if (!$message) {
            $message = "Time [$time1] should match time [$time2]";
        }
        $this->assertTrue(
                ($time1 == $time2) || ($time1 + 1 == $time2),
                $message);
    }
}
Cette ligne ne fait que demander à SimpleTest d'ignorer cette classe lors de la construction des suites de test. Elle peut être ajoutée n'importe où dans le fichier de scénario de test.

Les six succès ont l'air bien mais ne disent pas à un observateur peu attentif ce qui a été testé. Pour cela il faut regarder dans le code. Si cela vous paraît trop de boulot et que vous préfèreriez lire ces informations directement alors vous devriez aller lire comment afficher les succès.

Pour aller plus loin...