Du haut vers le bas et piloté par les tests

Commencer par la fantaisie, passer au code ensuite

J'ai menti.

Je n'ai pas créé de test pour le scripteur, uniquement l'interface FileWriter que j'ai affiché plus tôt. En fait je vais encore m'éloigner d'un article fini et présumer l'existence un scripteur abstrait dans classes/writer.php...

<?php
require_once(dirname(__FILE__) . '/simpletest/autorun.php');
require_once('../classes/log.php');
require_once('../classes/clock.php');
require_once('../classes/writer.php');
Mock::generate('Clock');
Mock::generate('Writer');

class TestOfLogging extends UnitTestCase {

    function testWriting() {
        $clock = new MockClock();
        $clock->returns('now', 'Timestamp');
        $writer = new MockWriter();
        $writer->expectOnce('write', array('[Timestamp] Test line'));
        $log = new Log($writer);
        $log->message('Test line', $clock);
    }
}
?>

Les changements correspondant au niveau du test sont...

<?php
require_once('../classes/log.php');
require_once('../classes/clock.php');
require_once('../classes/writer.php');
Mock::generate('Clock');
Mock::generate('Writer');

class TestOfLogging extends UnitTestCase {
    function TestOfLogging() {
        $this->UnitTestCase('Log class test');
    }
    function testWriting() {
        $clock = &new MockClock($this);
        $clock->setReturnValue('now', 'Timestamp');
        $writer = &new MockWriter($this);
        $writer->expectOnce('write', array('[Timestamp] Test line'));
        $log = &new Log($writer);
        $log->message('Test line', &$clock);
        $writer->tally();
    }
}
?>

Afin d'utiliser la classe de log, nous aurions besoin de coder un scripteur de fichier - ou un autre type de scripteur - mais pour le moment nous ne faisons que des tests et nous n'en avons pas encore besoin. En d'autres termes, en utilisant des objets fantaisie nous pouvons décaler la création d'un objet de niveau plus bas jusqu'au moment opportun. Non seulement nous pouvons faire la conception du haut vers le bas, mais en plus nous pouvons aussi tester du haut vers le bas.

S'approcher du bridge - pont

Imaginez pour un moment que nous ayons commencé la classe de log à partir d'une autre direction. Simulez avoir écrit juste assez du Log pour avoir réaliser le besoin d'un Writer. Comme l'aurions-nous inclus ?

Bon, l'héritage du scripteur ne nous aurait pas permis de le simuler du point de vue des tests. De celui de la conception nous aurions été restreint à un unique scripteur sans héritage multiple.

Créer un scripteur interne, plutôt qu'en le passant au constructeur, en choisissant un nom de classe, est possible, mais nous aurions moins de contrôle sur l'initialisation de l'objet fantaisie. Du point de vue de la conception il aurait été presque impossible de passer des paramètres au scripteur dans tous les formats possibles et envisageables. Vous auriez dû restreindre le scripteur à un hash ou à une chaîne compliquée décrivant tous les détails de la cible. Au mieux compliqué sans raison.

Utiliser une méthode fabrique pour créer le scripteur intérieurement serait possible, mais ça voudrait dire le sous classer pour les tests de manière à remplacer la méthode fabrique par une autre méthode renvoyant un leurre. Plus de boulot du point de vue des tests, quoique toujours possible. Du point de vue de la conception, ça voudrait dire créer une nouvelle sous-classe de log pour chaque type de scripteur. Cela s'appelle une hiérarchie de classe parallèle et fait bien entendu à de la duplication. Beurk.

A l'autre extrême, passer ou créer le scripteur à chaque message aurait été répétitif et aurait réduit le code de la classe Log à une unique méthode, un signe certain que toute la classe est devenue redondante.

Cette tension entre la facilité du test et le refus de la répétition nous a permis de trouver une conception à la fois flexible et propre. Vous vous souvenez de notre bref envie de l'héritage multiple ? Nous l'avons remplacé par du polymorphisme (plein de scripteurs) et séparé la hiérarchie du journal de celle de l'écriture. Nous relions les deux par agrégation à travers le plus simple Log. Cette astuce est en fait un design pattern (modèle de conception) appelé "Pont" ou "Bridge".

Donc nous avons été poussé par le code de test (nous n'avons presque rien écrit d'autre) vers un design pattern. Pensez-y une seconde. Les tests améliorent la qualité du code, à coup sûr dans mon cas, mais il y a quelque chose de bien plus profond et plus puissant.

Les tests ont amélioré la conception.

Simuler la conception

Créer un objet fantaisie est aussi simple que de créer l'interface à l'écrit. Si vous utilisez de l'UML ou d'autres outils pour générer ces interfaces alors vous avez un chemin encore plus flexible pour générer rapidement vos objets de test. Même sans, vous pouvez passer du dessin sur tableau blanc, à l'écriture de l'objet fantaisie, puis à la génération de l'interface qui nous renvoie de nouveau au tableau blanc, le tout très simplement. Comme le remaniement, la conception, le code et les tests s'unifient.

Parce que les objets fantaisie travaillent du haut vers le bas, ils peuvent être amenés dans la conception plus rapidement qu'un remaniement classique qui demande quant à lui du code fonctionnel avant de pourvoir s'installer. Ça veut dire que le code de test interagit plus vite avec la conception : par conséquent la qualité de la conception augmente elle aussi plus vite.

Un testeur unitaire est un outil de code. Un testeur unitaire avec objet fantaisie est un outil de conception.

Simuler maintenant, coder plus tard.
Nous dérivons vers le design pattern bridge.
Conception et test main dans la main.
Ce tutorial suit les classes frontière.
Vous aurez besoin du framework de test SimpleTest pour essayer ces exemples.
Pour des discussions plus fournis sur les objets fantaisie, voyez le Wiki des Extreme Tuesday ou le Wiki C2 (en anglais tous les deux).