Documentation sur les bouchons serveur

Cette page...

Que sont les bouchons serveur ?

Au départ il s'agit d'un modèle de conception initié par Robert Binder (Testing object-oriented systems: models, patterns, and tools, Addison-Wesley) in 1999. Un bouchon serveur est une simulation d'un objet ou d'un composant. Il doit remplacer exactement un composant dans un système pour des raisons de testabilité ou de prototypage, tout en restant léger. Il permet aux tests de tourner plus rapidement ou alors, si la classe simulée n'a pas été écrite, juste de fonctionner.

Créer des bouchons serveur

Nous avons juste besoin d'une classe préexistante, par exemple une connexion vers une base de données qui ressemblerait à...

class DatabaseConnection {
    function DatabaseConnection() {
    }
    
    function query() {
    }
    
    function selectQuery() {
    }
}
La classe n'a même pas encore besoin d'avoir été implémentée. Pour créer la version bouchonnée de cette classe, nous incluons la librairie de bouchon serveur et exécutons le générateur...
require_once('simpletest/mock_objects.php');
require_once('database_connection.php');
Stub::generate('DatabaseConnection');
Est généré un clone de la classe appelé StubDatabaseConnection. Nous pouvons alors créer des instances de cette nouvelle classe à l'intérieur de notre prototype de script...
require_once('simpletest/mock_objects.php');
require_once('database_connection.php');
Stub::generate('DatabaseConnection');

$connection = new StubDatabaseConnection();

La version bouchonnée de la classe contient toutes les méthodes de l'original de telle sorte qu'une opération comme $connection->query() soit encore légale. La valeur retournée sera null, Mais nous pouvons y remédier avec...
$connection->setReturnValue('query', 37)
Désormais à chaque appel de $connection->query() nous obtenons un résultat de 37. Nous pouvons choisir n'importe quelle valeur pour le résultat, par exemple un hash de résultats provenant d'un base de données imaginaire ou alors une liste d'objets persistants. Peu importe les paramètres, nous obtenons systématiquement les même valeurs chaque fois qu'ils ont été initialisés de la sorte : ça ne ressemble peut-être pas à une réponse convaincante venant d'une connexion vers une base de données. Mais pour la demi-douzaine de lignes d'une méthode de test c'est souvent largement suffisant.

Modèles de simulation

Sauf que les choses ne sont que rarement aussi simples. Parmi les problèmes les plus courants on trouve les itérateurs : le renvoi d'une valeur constante peut causer une boucle infini dans l'objet testé. Pour ceux-ci nous avons besoin de mettre sur pied une suite de valeurs. Prenons par exemple un itérateur simple qui ressemble à...

class Iterator {
    function Iterator() {
    }
    
    function next() {
    }
}
C'est probablement le plus simple des itérateurs possibles. Supposons que cet itérateur ne retourne que du texte, jusqu'à la fin - quand il retourne false. Une simulation est possible avec...
Stub::generate('Iterator');

$iterator = new StubIterator();
$iterator->setReturnValue('next', false);
$iterator->setReturnValueAt(0, 'next', 'First string');
$iterator->setReturnValueAt(1, 'next', 'Second string');
A l'appel de next() sur l'itérateur bouchonné il va d'abord renvoyer "First string", puis au second appel c'est "Second string" qui sera renvoyé. Finalement pour tous les autres appels, il s'agira d'un false. Les valeurs renvoyées successivement ont priorité sur la valeur constante renvoyé. Cette dernière est un genre de valeur par défaut.

Une autre situation délicate est une opération get() surchargée. Un exemple ? Un porteur d'information avec des pairs de clef / valeur. Prenons une classe de configuration...

class Configuration {
    function Configuration() {
    }
    
    function getValue($key) {
    }
}
Il s'agit d'une situation propice à l'utilisation d'objets bouchon, surtout que la configuration en production dépend invariablement de la machine : l'utiliser directement ne va pas nous aider à maintenir notre confiance dans nos tests. Sauf que le problème tient de ce que toutes les données proviennent de la méthode getValue() et que nous voulons des résultats différents suivant la clef. Par chance les bouchons ont un système de filtre...
Stub::generate('Configuration');

$config = &new StubConfiguration();
$config->setReturnValue('getValue', 'primary', array('db_host'));
$config->setReturnValue('getValue', 'admin', array('db_user'));
$config->setReturnValue('getValue', 'secret', array('db_password'));
Ce paramètre supplémentaire est une liste d'arguments que l'on peut utiliser. Dans ce cas nous essayons d'utiliser un unique argument, à savoir la clef recherchée. Maintenant quand on invoque le bouchon serveur via la méthode getValue() avec...
$config->getValue('db_user');
...il renvoie "admin". Il le trouve en essayant d'assortir successivement les arguments d'entrée avec sa liste de ceux de sortie jusqu'au moment où une correspondance exacte est trouvée.

Vous pouvez définir un argument par défaut avec...


$config->setReturnValue('getValue', false, array('*'));
Attention ce n'est pas équivalent à initialiser la valeur retournée sans aucun argument.

$config->setReturnValue('getValue', false);
Dans le premier cas il acceptera n'importe quel argument, mais exactement un -- pas plus -- est nécessaire. Dans le second cas n'importe quel nombre d'arguments fera l'affaire : il agit comme un catchall après tous les correspondances. Prenez garde à l'ordre : si nous ajoutons un autre paramètre après le joker ('*') il sera ignoré puisque le joker aura été trouvé auparavant. Avec des listes de paramètres complexes l'ordre peut devenir crucial, au risque de perdre des correspondances souhaitées, masquées par un joker antérieur. Pensez à mettre d'abord les cas les plus spécifiques si vous n'êtes pas sûr.

Il y a des fois où l'on souhaite qu'un objet spécifique soit servi par le bouchon plutôt qu'une simple copie. La sémantique de la copie en PHP nous force à utiliser une autre méthode pour cela. Vous êtes peut-être en train de simuler un conteneur par exemple...

class Thing {
}

class Vector {
    function Vector() {
    }
    
    function get($index) {
    }
}
Dans ce cas vous pouvez mettre une référence dans la liste renvoyée par le bouchon...
Stub::generate('Vector');

$thing = new Thing();
$vector = &new StubVector();
$vector->setReturnReference('get', $thing, array(12));
Avec ce petit arrangement vous vous assurez qu'à chaque fois que $vector->get(12) est appelé il renverra le même $thing.

Ces trois facteurs, ordre, paramètres et copie (ou référence), peuvent être combinés orthogonalement. Par exemple...

$complex = &new StubComplexThing();
$stuff = new Stuff();
$complex->setReturnReferenceAt(3, 'get', $stuff, array('*', 1));
Le $stuff ne sera renvoyé qu'au troisième appel et seulement si deux paramètres étaient indiqués, avec la contrainte que le second de ceux-ci soit l'entier 1. N'est-ce pas suffisant pour des situations de prototypage simple ?

Un dernier cas critique reste celle d'un objet en créant un autre, connu sous le nom du modèle factory - fabrique. Supposons qu'après une requête réussie à notre base de données imaginaire, un ensemble de résultats est retourné sous la forme d'un itérateur, chaque appel à next() donnant un ligne et à la fin un false. Au premier abord, ça donne l'impression d'être cauchemardesque à simuler. Alors qu'en fait tout peut être bouchonné en utilisant les mécanismes ci-dessus.

Voici comment...

Stub::generate('DatabaseConnection');
Stub::generate('ResultIterator');

class DatabaseTest extends UnitTestCase {
    
    function testUserFinder() {
        $result = &new StubResultIterator();
        $result->setReturnValue('next', false);
        $result->setReturnValueAt(0, 'next', array(1, 'tom'));
        $result->setReturnValueAt(1, 'next', array(3, 'dick'));
        $result->setReturnValueAt(2, 'next', array(6, 'harry'));
        
        $connection = &new StubDatabaseConnection();
        $connection->setReturnValue('query', false);
        $connection->setReturnReference(
                'query',
                $result,
                array('select id, name from users'));
                
        $finder = &new UserFinder($connection);
        $this->assertIdentical(
                $finder->findNames(),
                array('tom', 'dick', 'harry'));
    }
}
Désormais ce n'est que si notre $connection est appelé avec la bonne query() que le $result sera renvoyé après le troisième appel à next(). Cela devrait être suffisant pour que notre classe UserFinder, la classe effectivement testée à ce niveau, puisse s'exécuter comme il faut. Un test très précis et pas une seule base de données à l'horizon.

Options de création pour les bouchons

Il y a d'autres options additionnelles à la création d'un bouchon. Au moment de la génération nous pouvons changer le nom de la classe...

Stub::generate('Iterator', 'MyStubIterator');
$iterator = &new MyStubIterator();

Pris tout seul ce n'est pas très utile étant donné qu'il n'y aurait pas de différence entre cette classe et celle par défaut -- à part le nom bien entendu. Par contre nous pouvons aussi lui ajouter d'autres méthodes qui ne se trouveraient pas dans l'interface originale...
class Iterator {
}
Stub::generate('Iterator', 'PrototypeIterator', array('next', 'isError'));
$iterator = &new PrototypeIterator();
$iterator->setReturnValue('next', 0);

Les méthodes next() et isError() peuvent maintenant renvoyer des ensembles de valeurs exactement comme si elles existaient dans la classe originale.

Un moyen encore plus ésotérique de modifier les bouchons est de changer le joker utiliser par défaut pour la correspondance des paramètres.

Stub::generate('Connection');
$iterator = &new StubConnection('wild');
$iterator->setReturnValue('query', array('id' => 33), array('wild'));

L'unique raison valable pour effectuer cette opération, c'est quand vous souhaitez tester la chaîne "*" sans pour autant l'interpréter comme un "n'importe lequel".

Pour aller plus loin...