Documentation sur les attentes

This page...

Plus de contrôle sur les objets fantaisie

Le comportement par défaut des objets fantaisie dans SimpleTest est soit une correspondance identique sur l'argument, soit l'acceptation de n'importe quel argument. Pour la plupart des tests, c'est suffisant. Cependant il est parfois nécessaire de ramollir un scénario de test.

Un des endroits où un test peut être trop serré est la reconnaissance textuelle. Prenons l'exemple d'un composant qui produirait un message d'erreur utile lorsque quelque chose plante. Il serait utile de tester que l'erreur correcte est renvoyée, mais le texte proprement dit risque d'être plutôt long. Si vous testez le texte dans son ensemble alors à chaque modification de ce même message -- même un point ou une virgule -- vous aurez à revenir sur la suite de test pour la modifier.

Voici un cas concret, nous avons un service d'actualités qui a échoué dans sa tentative de connexion à sa source distante.

class NewsService {
    ...
    function publish($writer) {
        if (! $this->isConnected()) {
            $writer->write('Cannot connect to news service "' .
                    $this->_name . '" at this time. ' .
                    'Please try again later.');
        }
        ...
    }
}
Là il envoie son contenu vers un classe Writer. Nous pourrions tester ce comportement avec un MockWriter...
class TestOfNewsService extends UnitTestCase {
    ...
    function testConnectionFailure() {
        $writer = new MockWriter($this);
        $writer->expectOnce('write', array(
                'Cannot connect to news service ' .
                '"BBC News" at this time. ' .
                'Please try again later.'));
        
        $service = new NewsService('BBC News');
        $service->publish($writer);
        
        $writer->tally();
    }
}
C'est un bon exemple d'un test fragile. Si nous décidons d'ajouter des instructions complémentaires, par exemple proposer une source d'actualités alternative, nous casserons nos tests par la même occasion sans pourtant avoir modifié une seule fonctionnalité.

Pour contourner ce problème, nous voudrions utiliser un test avec une expression rationnelle plutôt qu'une correspondance exacte. Nous pouvons y parvenir avec...

class TestOfNewsService extends UnitTestCase {
    ...
    function testConnectionFailure() {
        $writer = new MockWriter($this);
        $writer->expectOnce(
                'write',
                array(new PatternExpectation('/cannot connect/i')));
        
        $service = new NewsService('BBC News');
        $service->publish($writer);
        
        $writer->tally();
    }
}
Plutôt que de transmettre le paramètre attendu au MockWriter, nous envoyons une classe d'attente appelée PatternExpectation. L'objet fantaisie est suffisamment élégant pour reconnaître qu'il s'agit d'un truc spécial et pour le traiter différemment. Plutôt que de comparer l'argument entrant à cet objet, il utilise l'objet attente lui-même pour exécuter le test.

PatternExpectation utilise l'expression rationnelle pour la comparaison avec son constructeur. A chaque fois qu'une comparaison est fait à travers MockWriter par rapport à cette classe attente, elle fera un preg_match() avec ce motif. Dans notre scénario de test ci-dessus, aussi longtemps que la chaîne "cannot connect" apparaît dans le texte, la fantaisie transmettra un succès au testeur unitaire. Peu importe le reste du texte.

Les classes attente possibles sont...
AnythingExpectation Sera toujours validé
EqualExpectation Une égalité, plutôt que la plus forte comparaison à l'identique
NotEqualExpectation Une comparaison sur la non-égalité
IndenticalExpectation La vérification par défaut de l'objet fantaisie qui doit correspondre exactement
NotIndenticalExpectation Inverse la logique de l'objet fantaisie
PatternExpectation Utilise une expression rationnelle Perl pour comparer une chaîne
NoPatternExpectation Passe seulement si l'expression rationnelle Perl échoue
IsAExpectation Vérifie le type ou le nom de la classe uniquement
NotAExpectation L'opposé de IsAExpectation
MethodExistsExpectation Vérifie si la méthode est disponible sur un objet
TrueExpectation Accepte n'importe quelle variable PHP qui vaut vrai
FalseExpectation Accepte n'importe quelle variable PHP qui vaut faux
La plupart utilisent la valeur attendue dans le constructeur. Les exceptions sont les vérifications sur motif, qui utilisent une expression rationnelle, ainsi que IsAExpectation et NotAExpectation, qui prennent un type ou un nom de classe comme chaîne.

Utiliser les attentes pour contrôler les bouchons serveur

Les classes attente peuvent servir à autre chose que l'envoi d'assertions depuis les objets fantaisie, afin de choisir le comportement d'un objet fantaisie ou celui d'un bouchon serveur. A chaque fois qu'une liste d'arguments est donnée, une liste d'objets d'attente peut être insérée à la place.

Mettons que nous voulons qu'un bouchon serveur d'autorisation simule une connexion réussie seulement si il reçoit un objet de session valide. Nous pouvons y arriver avec ce qui suit...

Stub::generate('Authorisation');

$authorisation = new StubAuthorisation();
$authorisation->returns(
        'isAllowed',
        true,
        array(new IsAExpectation('Session', 'Must be a session')));
$authorisation->returns('isAllowed', false);
Le comportement par défaut du bouchon serveur est défini pour renvoyer false quand isAllowed est appelé. Lorsque nous appelons cette méthode avec un unique paramètre qui est un objet Session, il renverra true. Nous avons aussi ajouté un deuxième paramètre comme message. Il sera affiché dans le message d'erreur de l'objet fantaisie si l'attente est la cause de l'échec.

Ce niveau de sophistication est rarement utile : il n'est inclut que pour être complet.

Créer vos propres attentes

Les classes d'attentes ont une structure très simple. Tellement simple qu'il devient très simple de créer vos propres version de logique pour des tests utilisés couramment.

Par exemple voici la création d'une classe pour tester la validité d'adresses IP. Pour fonctionner correctement avec les bouchons serveurs et les objets fantaisie, cette nouvelle classe d'attente devrait étendre SimpleExpectation ou une autre de ses sous-classes...

class ValidIp extends SimpleExpectation {
    
    function test($ip) {
        return (ip2long($ip) != -1);
    }
    
    function testMessage($ip) {
        return "Address [$ip] should be a valid IP address";
    }
}
Il n'y a véritablement que deux méthodes à mettre en place. La méthode test() devrait renvoyer un true si l'attente doit passer, et une erreur false dans le cas contraire. La méthode testMessage() ne devrait renvoyer que du texte utile à la compréhension du test en lui-même.

Cette classe peut désormais être employée à la place des classes d'attente précédentes.

Voici un exemple plus typique, vérifier un hash...

class JustField extends EqualExpectation {
    private $key;
    
    function __construct($key, $expected) {
        parent::__construct($expected);
        $this->key = $key;
    }
    
    function test($compare) {
        if (! isset($compare[$this->key])) {
            return false;
        }
        return parent::test($compare[$this->key]);
    }
    
    function testMessage($compare) {
        if (! isset($compare[$this->key])) {
            return 'Key [' . $this->key . '] does not exist';
        }
        return 'Key [' . $this->key . '] -> ' .
                parent::testMessage($compare[$this->key]);
    }
}
L'habitude a été prise pour séparer les clauses du message avec " -> ". Cela permet aux outils dérivés de reformater la sortie.

Supposons qu'un authentificateur s'attend à recevoir une ligne de base de données correspondant à l'utilisateur, et que nous avons juste besoin de valider le nom d'utilisateur. Nous pouvons le faire très simplement avec...

$mock->expectOnce('authenticate',
                  array(new JustKey('username', 'marcus')));

Sous le capot du testeur unitaire

Le framework de test unitaire SimpleTest utilise aussi dans son coeur des classes d'attente pour la classe UnitTestCase. Nous pouvons aussi tirer parti de ces mécanismes pour réutiliser nos propres classes attente à l'intérieur même des suites de test.

La méthode la plus directe est d'utiliser la méthode SimpleTest::assert() pour effectuer le test...

class TestOfNetworking extends UnitTestCase {
    ...
    function testGetValidIp() {
        $server = &new Server();
        $this->assert(
                new ValidIp(),
                $server->getIp(),
                'Server IP address->%s');
    }
}
assert() testera toute attente directement.

C'est plutôt sale par rapport à notre syntaxe habituelle du type assert...().

Pour un cas aussi simple, nous créons d'ordinaire une méthode d'assertion distincte en utilisant la classe d'attente. Supposons un instant que notre attente soit un peu plus compliquée et que par conséquent nous souhaitions la réutiliser, nous obtenons...

class TestOfNetworking extends UnitTestCase {
    ...
    function assertValidIp($ip, $message = '%s') {
        $this->assertExpectation(new ValidIp(), $ip, $message);
    }
    
    function testGetValidIp() {
        $server = &new Server();
        $this->assertValidIp(
                $server->getIp(),
                'Server IP address->%s');
    }
}
It is rare to need the expectations for more than pattern matching, but these facilities do allow testers to build some sort of domain language for testing their application. Also, complex expectation classes could make the tests harder to read and debug. In effect extending the test framework to create their own tool set. Il est assez rare que le besoin d'une attente dépasse le stade de la reconnaissance d'un motif, mais ils permettent aux testeurs de construire leur propre langage dédié ou DSL (Domain Specific Language) pour tester leurs applications. Attention, les classes d'attente complexes peuvent rendre les tests difficiles à lire et à déboguer. Ces mécanismes sont véritablement là pour les auteurs de système qui étendront le framework de test pour leurs propres outils de test.

References and related information...