<?xml version="1.0" encoding="ISO-8859-1" ?>
<!-- $Id: partial_mocks_documentation.xml 1701 2008-03-24 20:08:06Z pp11 $ -->
<page title="Documentation sur les objets fantaisie partiels" here="Les objets fantaisie partiels">
    <synchronisation lang="en" version="1687" date="24/03/2008" maintainer="pp11" />
    <long_title>Documentation SimpleTest : les objets fantaisie partiels</long_title>
    <content>
        <introduction>
            <p>
                Un objet fantaisie partiel n'est ni plus ni moins
                qu'un modèle de conception pour soulager un problème spécifique
                du test avec des objets fantaisie, celui de placer
                des objets fantaisie dans des coins serrés.
                Il s'agit d'un outil assez limité et peut-être même
                une idée pas si bonne que ça. Elle est incluse dans SimpleTest
                pour la simple raison que je l'ai trouvée utile
                à plus d'une occasion et qu'elle m'a épargnée
                pas mal de travail dans ces moments-là.
            </p>
        </introduction>
        <section name="injection" title="Le problème de l'injection dans un objet fantaisie">
            <p>
                Quand un objet en utilise un autre il est très simple
                d'y faire circuler une version fantaisie déjà prête
                avec ses attentes. Les choses deviennent un peu plus délicates
                si un objet en crée un autre et que le créateur est celui
                que l'on souhaite tester. Cela revient à dire que l'objet
                créé devrait être une fantaisie, mais nous pouvons
                difficilement dire à notre classe sous test de créer
                un objet fantaisie plutôt qu'un &quot;vrai&quot; objet.
                La classe testée ne sait même pas qu'elle travaille dans un environnement de test.
            </p>
            <p>
                Par exemple, supposons que nous sommes en train
                de construire un client telnet et qu'il a besoin
                de créer une socket réseau pour envoyer ses messages.
                La méthode de connexion pourrait ressemble à quelque chose comme...
<php><![CDATA[
<strong><?php
require_once('socket.php');

class Telnet {
    ...
    function &connect($ip, $port, $username, $password) {
        $socket = &new Socket($ip, $port);
        $socket->read( ... );
        ...
    }
}
?></strong>
]]></php>
                Nous voudrions vraiment avoir une version fantaisie
                de l'objet socket, que pouvons nous faire ?
            </p>
            <p>
                La première solution est de passer la socket en
                tant que paramètre, ce qui force la création
                au niveau inférieur. Charger le client de cette tâche
                est effectivement une bonne approche si c'est possible
                et devrait conduire à un remaniement -- de la création
                à partir de l'action. En fait, c'est là une des manières
                avec lesquels tester en s'appuyant sur des objets fantaisie
                vous force à coder des solutions plus resserrées sur leur objectif.
                Ils améliorent votre programmation.
            </p>
            <p>
                Voici ce que ça devrait être...
<php><![CDATA[
<?php
require_once('socket.php');

class Telnet {
    ...
    <strong>function &connect(&$socket, $username, $password) {
        $socket->read( ... );
        ...
    }</strong>
}
?>
]]></php>
                Sous-entendu, votre code de test est typique d'un cas
                de test avec un objet fantaisie.
<php><![CDATA[
class TelnetTest extends UnitTestCase {
    ...
    function testConnection() {<strong>
        $socket = &new MockSocket($this);
        ...
        $telnet = &new Telnet();
        $telnet->connect($socket, 'Me', 'Secret');
        ...</strong>
    }
}
]]></php>
                C'est assez évident que vous ne pouvez descendre que d'un niveau.
                Vous ne voudriez pas que votre application de haut niveau
                crée tous les fichiers de bas niveau, sockets et autres connexions
                à la base de données dont elle aurait besoin.
                Elle ne connaîtrait pas les paramètres du constructeur de toute façon.
            </p>
            <p>
                La solution suivante est de passer l'objet créé sous la forme
                d'un paramètre optionnel...
<php><![CDATA[
<?php
require_once('socket.php');

class Telnet {
    ...<strong>
    function &connect($ip, $port, $username, $password, $socket = false) {
        if (!$socket) {
            $socket = &new Socket($ip, $port);
        }
        $socket->read( ... );</strong>
        ...
        return $socket;
    }
}
?>
]]></php>
                Pour une solution rapide, c'est généralement suffisant.
                Ensuite le test est très similaire : comme si le paramètre
                était transmis formellement...
<php><![CDATA[
class TelnetTest extends UnitTestCase {
    ...
    function testConnection() {<strong>
        $socket = &new MockSocket($this);
        ...
        $telnet = &new Telnet();
        $telnet->connect('127.0.0.1', 21, 'Me', 'Secret', &$socket);
        ...</strong>
    }
}
]]></php>
                Le problème de cette approche tient dans son manque de netteté.
                Il y a du code de test dans la classe principale et aussi
                des paramètres transmis dans le scénario de test
                qui ne sont jamais utilisés. Il s'agit là d'une approche
                rapide et sale, mais qui ne reste pas moins efficace
                dans la plupart des situations.
            </p>
            <p>
                Une autre solution encore est de laisser un objet fabrique
                s'occuper de la création...
<php><![CDATA[
<?php
require_once('socket.php');

class Telnet {<strong>
    function Telnet(&$network) {
        $this->_network = &$network;
    }</strong>
    ...
    function &connect($ip, $port, $username, $password) {<strong>
        $socket = &$this->_network->createSocket($ip, $port);
        $socket->read( ... );</strong>
        ...
        return $socket;
    }
}
?>
]]></php>
                Il s'agit là probablement de la réponse la plus travaillée
                étant donné que la création est maintenant située
                dans une petite classe spécialisée. La fabrique réseau
                peut être testée séparément et utilisée en tant que fantaisie
                quand nous testons la classe telnet...
<php><![CDATA[
class TelnetTest extends UnitTestCase {
    ...
    function testConnection() {<strong>
        $socket = &new MockSocket($this);
        ...
        $network = &new MockNetwork($this);
        $network->setReturnReference('createSocket', $socket);
        $telnet = &new Telnet($network);
        $telnet->connect('127.0.0.1', 21, 'Me', 'Secret');
        ...</strong>
    }
}
]]></php>
                Le problème reste que nous ajoutons beaucoup de classes
                à la bibliothèque. Et aussi que nous utilisons beaucoup
                de fabriques ce qui rend notre code un peu moins intuitif.
                La solution la plus flexible, mais aussi la plus complexe.
            </p>
            <p>
                Peut-on trouver un juste milieu ?
            </p>
        </section>
        <section name="creation" title="Méthode fabrique protégée">
            <p>
                Il existe une technique pour palier à ce problème
                sans créer de nouvelle classe dans l'application;
                par contre elle induit la création d'une sous-classe au moment du test.
                Premièrement nous déplaçons la création de la socket dans sa propre méthode...
<php><![CDATA[
<?php
require_once('socket.php');

class Telnet {
    ...
    function &connect($ip, $port, $username, $password) {<strong>
        $socket = &$this->_createSocket($ip, $port);</strong>
        $socket->read( ... );
        ...
    }<strong>
    
    function &_createSocket($ip, $port) {
        return new Socket($ip, $port);
    }</strong>
}
?>
]]></php>
                Il s'agit là de la seule modification dans le code de l'application.
            </p>
            <p>
                Pour le scénario de test, nous devons créer
                une sous-classe de manière à intercepter la création de la socket...
<php><![CDATA[
<strong>class TelnetTestVersion extends Telnet {
    var $_mock;
    
    function TelnetTestVersion(&$mock) {
        $this->_mock = &$mock;
        $this->Telnet();
    }
    
    function &_createSocket() {
        return $this->_mock;
    }
}</strong>
]]></php>
                Ici j'ai déplacé la fantaisie dans le constructeur,
                mais un setter aurait fonctionné tout aussi bien.
                Notez bien que la fantaisie est placée dans une variable
                d'objet avant que le constructeur ne soit attaché.
                C'est nécessaire dans le cas où le constructeur appelle 
                <code>connect()</code>.
                Autrement il pourrait donner un valeur nulle à partir de
                <code>_createSocket()</code>.
            </p>
            <p>
                Après la réalisation de tout ce travail supplémentaire
                le scénario de test est assez simple.
                Nous avons juste besoin de tester notre nouvelle classe à la place...
<php><![CDATA[
class TelnetTest extends UnitTestCase {
    ...
    function testConnection() {<strong>
        $socket = &new MockSocket($this);
        ...
        $telnet = &new TelnetTestVersion($socket);
        $telnet->connect('127.0.0.1', 21, 'Me', 'Secret');
        ...</strong>
    }
}
]]></php>
                Cette nouvelle classe est très simple bien sûr.
                Elle ne fait qu'initier une valeur renvoyée, à la manière
                d'une fantaisie. Ce serait pas mal non plus si elle pouvait
                vérifier les paramètres entrants.
                Exactement comme un objet fantaisie.
                Il se pourrait bien que nous ayons à réaliser cette astuce régulièrement :
                serait-il possible d'automatiser la création de cette sous-classe ?
            </p>
        </section>
        <section name="partiel" title="Un objet fantaisie partiel">
            <p>
                Bien sûr la réponse est &quot;oui&quot;
                ou alors j'aurais arrêté d'écrire depuis quelques temps déjà !
                Le test précédent a représenté beaucoup de travail,
                mais nous pouvons générer la sous-classe en utilisant
                une approche à celle des objets fantaisie.
            </p>
            <p>
                Voici donc une version avec objet fantaisie partiel du test...
<php><![CDATA[
<strong>Mock::generatePartial(
        'Telnet',
        'TelnetTestVersion',
        array('_createSocket'));</strong>

class TelnetTest extends UnitTestCase {
    ...
    function testConnection() {<strong>
        $socket = &new MockSocket($this);
        ...
        $telnet = &new TelnetTestVersion($this);
        $telnet->setReturnReference('_createSocket', $socket);
        $telnet->Telnet();
        $telnet->connect('127.0.0.1', 21, 'Me', 'Secret');
        ...</strong>
    }
}
]]></php>
                La fantaisie partielle est une sous-classe de l'original
                dont on aurait &quot;remplacé&quot; les méthodes sélectionnées
                avec des versions de test. L'appel à <code>generatePartial()</code>
                nécessite trois paramètres : la classe à sous classer,
                le nom de la nouvelle classe et une liste des méthodes à simuler.
            </p>
            <p>
                Instancier les objets qui en résultent est plutôt délicat.
                L'unique paramètre du constructeur d'un objet fantaisie partiel
                est la référence du testeur unitaire.
                Comme avec les objets fantaisie classiques c'est nécessaire
                pour l'envoi des résultats de test en réponse à la vérification des attentes.
            </p>
            <p>
                Une nouvelle fois le constructeur original n'est pas lancé.
                Indispensable dans le cas où le constructeur aurait besoin
                des méthodes fantaisie : elles n'ont pas encore été initiées !
                Nous initions les valeurs retournées à cet instant et
                ensuite lançons le constructeur avec ses paramètres normaux.
                Cette construction en trois étapes de &quot;new&quot;,
                suivie par la mise en place des méthodes et ensuite
                par la lancement du constructeur proprement dit est
                ce qui distingue le code d'un objet fantaisie partiel.
            </p>
            <p>
                A part pour leur construction, toutes ces méthodes
                fantaisie ont les mêmes fonctionnalités que dans
                le cas des objets fantaisie et toutes les méthodes
                non fantaisie se comportent comme avant.
                Nous pouvons mettre en place des attentes très facilement...
<php><![CDATA[
class TelnetTest extends UnitTestCase {
    ...
    function testConnection() {
        $socket = &new MockSocket($this);
        ...
        $telnet = &new TelnetTestVersion($this);
        $telnet->setReturnReference('_createSocket', $socket);<strong>
        $telnet->expectOnce('_createSocket', array('127.0.0.1', 21));</strong>
        $telnet->Telnet();
        $telnet->connect('127.0.0.1', 21, 'Me', 'Secret');
        ...<strong>
        $telnet->tally();</strong>
    }
}
]]></php>
            </p>
        </section>
        <section name="moins" title="Tester moins qu'une classe">
            <p>
                Les méthodes issues d'un objet fantaisie n'ont pas
                besoin d'être des méthodes fabrique, Il peut s'agir
                de n'importe quelle sorte de méthode.
                Ainsi les objets fantaisie partiels nous permettent
                de prendre le contrôle de n'importe quelle partie d'une classe,
                le constructeur excepté. Nous pourrions même aller jusqu'à
                créer des fantaisies sur toutes les méthodes à part celle
                que nous voulons effectivement tester.
            </p>
            <p>
                Cette situation est assez hypothétique, étant donné
                que je ne l'ai jamais essayée. Je suis ouvert à cette possibilité,
                mais je crains qu'en forçant la granularité d'un objet
                on n'obtienne pas forcément un code de meilleur qualité.
                Personnellement j'utilise les objets fantaisie partiels
                comme moyen de passer outre la création ou alors
                de temps en temps pour tester le modèle de conception TemplateMethod.
            </p>
            <p>
                Pour choisir le mécanisme à utiliser, on en revient
                toujours aux standards de code de votre projet.
            </p>
        </section>
    </content>
    <internal>
        <link>
            <a href="#injection">Le problème de l'injection d'un objet fantaisie</a>.
        </link>
        <link>
            Déplacer la création vers une méthode <a href="#creation">fabrique protégée</a>.
        </link>
        <link>
            <a href="#partiel">L'objet fantaisie partiel</a> génère une sous-classe.
        </link>
        <link>
            Les objets fantaisie partiels <a href="#moins">testent moins qu'une classe</a>.
        </link>
    </internal>
    <external>
        <link>
            La page du projet SimpleTest sur
            <a href="http://sourceforge.net/projects/simpletest/">SourceForge</a>.
        </link>
        <link>
            <a href="http://simpletest.org/api/">L'API complète pour SimpleTest</a>
            à partir de PHPDoc.
        </link>
        <link>
            La méthode fabrique protégée est décrite dans
            <a href="http://www-106.ibm.com/developerworks/java/library/j-mocktest.html">
            cet article d'IBM</a>. Il s'agit de l'unique papier
            formel que j'ai vu sur ce problème.
        </link>
    </external>
    <meta>
        <keywords>
            développement logiciel en php,
            dévelopement d'un scénario de test en php,
            programmation php avec base de données,
            outils de développement logiciel,
            tutoriel avancé en php,
            scripts à la manière de phpunit,
            architecture,
            ressources php,
            objets fantaisie,
            junit,
            framework de test en php,
            test unitaire,
            test en php
        </keywords>
    </meta>
</page>

