Les objets fantaisie - ou "mock objects" en anglais - ont deux rôles pendant un scénario de test : acteur et critique.
Le comportement d'acteur est celui de simuler des objets difficiles à initialiser ou trop consommateurs en temps pendant un test. Le cas classique est celui de la connexion à une base de données. Mettre sur pied une base de données de test au lancement de chaque test ralentirait considérablement les tests et en plus exigerait l'installation d'un moteur de base de données ainsi que des données sur la machine de test. Si nous pouvons simuler la connexion et renvoyer des données à notre guise alors non seulement nous gagnons en pragmatisme sur les tests mais en sus nous pouvons nourrir notre base avec des données falsifiées et voir comment il répond. Nous pouvons simuler une base de données en suspens ou d'autres cas extrêmes sans avoir à créer une véritable panne de base de données. En d'autres termes nous pouvons gagner en contrôle sur l'environnement de test.
Si les objets fantaisie ne se comportaient que comme des acteurs alors on les connaîtrait sous le nom de bouchons serveur.
Cependant non seulement les objets fantaisie jouent un rôle (en fournissant à la demande les valeurs requises) mais en plus ils sont aussi sensibles aux messages qui leur sont envoyés (par le biais d'attentes). En posant les paramètres attendus d'une méthode ils agissent comme des gardiens : un appel sur eux doit être réalisé correctement. Si les attentes ne sont pas atteintes ils nous épargnent l'effort de l'écriture d'une assertion de test avec échec en réalisant cette tâche à notre place. Dans le cas d'une connexion à une base de données imaginaire ils peuvent tester si la requête, disons SQL, a bien été formé par l'objet qui utilise cette connexion. Mettez-les sur pied avec des attentes assez précises et vous verrez que vous n'aurez presque plus d'assertion à écrire manuellement.
Comme pour la création des bouchons serveur, tout ce dont nous avons besoin c'est d'un classe existante. La fameuse connexion à une base de données qui ressemblerait à...
MockDatabaseConnection. Nous pouvons désormais créer des instances de cette nouvelle classe à l'intérieur même de notre scénario de test...
La version fantaisie d'une classe contient toutes les méthodes de l'originale. De la sorte une opération comme query()]]> est encore possible. Tout comme avec les bouchons, nous pouvons remplacer la valeur nulle renvoyée par défaut...
query()]]> nous recevons comme résultat 37. Tout comme avec les bouchons nous pouvons utiliser des jokers et surcharger le paramètre joker. Nous pouvons aussi ajouter des méthodes supplémentaires à l'objet fantaisie lors de sa génération et lui choisir un nom de classe qui lui soit propre...
setOptions() existait dans la classe originale. C'est pratique si une classe a utilisé le mécanisme overload() de PHP pour ajouter des méthodes dynamiques. Vous pouvez créer des fantaisies spéciales pour simuler cette situation.
Tous les modèles disponibles avec les bouchons serveur le sont également avec les objets fantaisie...
false. Nous pouvons le simuler avec...
next() sur l'itérateur fantaisie il renverra tout d'abord "First string", puis ce sera au tour de "Second string" au deuxième appel et ensuite pour tout appel suivant false sera renvoyé. Ces valeurs renvoyées successivement sont prioritaires sur la valeur constante retournée. Cette dernière est un genre de valeur par défaut si vous voulez.
Reprenons aussi le conteneur d'information bouchonné avec des pairs clef / valeur...
getValue() et que nous voulons des résultats différents pour des clefs différentes. Heureusement les objets fantaisie ont un système de filtrage...
getValue() est invoquée sur l'objet fantaisie...
Il y a des fois où vous souhaitez qu'un objet spécifique soit servi par la fantaisie plutôt qu'une copie. De nouveau c'est identique au mécanisme des bouchons serveur...
get(12)]]> le même $thing sera renvoyé.
Même si les bouchons serveur vous isolent du désordre du monde réel, il ne s'agit là que de la moitié du bénéfice potentiel. Vous pouvez avoir une classe de test recevant les messages ad hoc, mais est-ce que votre nouvelle classe renvoie bien les bons ? Le tester peut devenir cafouillis sans une librairie d'objets fantaisie.
Pour l'exemple, prenons une classe SessionPool à laquelle nous allons ajouter une fonction de log. Plutôt que de complexifier la classe originale, nous souhaitons ajouter ce comportement avec un décorateur (GOF). Pour l'instant le code de SessionPool ressemble à...
LoggingSessionPool. En particulier, nous voulons vérifier que la méthode findSession() est appelée avec le bon identifiant de session au sein du cookie et qu'elle renvoie bien le message "Starting session $cookie" au loggueur.
Bien que nous ne testions que quelques lignes de code de production, voici la liste des choses à faire dans un scénario de test conventionnel :
SessionPool.findSession().
A la place, voici la méthode complète pour le test avec un peu de magie via les objets fantaisie...
findSession() est un méthode fabrique dont la simulation est décrite plus haut. Le point de départ vient avec le premier appel expectOnce(). Cette ligne indique qu'à chaque fois que findSession() est invoqué sur l'objet fantaisie, il vérifiera les arguments entrant. S'il ne reçoit que la chaîne "abc" en tant qu'argument alors un succès est envoyé au testeur unitaire, sinon c'est un échec qui est généré. Il s'agit là de la partie qui teste si nous avons bien la bonne session. La liste des arguments suit une format identique à celui qui précise les valeurs renvoyées. Vous pouvez avoir des jokers et des séquences et l'ordre de l'évaluation restera le même.
Si l'appel n'est jamais effectué alors n'est généré ni le succès, ni l'échec. Pour contourner cette limitation, nous devons dire à l'objet fantaisie que le test est terminé : il pourra alors décider si les attentes ont été répondues. L'assertion du testeur unitaire de ceci est déclenchée par l'appel tally() à la fin du test.
Nous utilisons le même modèle pour mettre sur pied le loggueur fantaisie. Nous lui indiquons que message() devrait être invoqué une fois et une fois seulement avec l'argument "Starting session abc". En testant les arguments d'appel, plutôt que ceux de sortie du loggueur, nous isolons le test de tout modification dans le loggueur.
Nous commençons le lancement nos tests à la création du nouveau LoggingSessionPool et nous l'alimentons avec nos objets fantaisie juste créés. Désormais tout est sous contrôle. Au final nous confirmons que le $session donné au décorateur est bien celui reçu et prions les objets fantaisie de lancer leurs tests de comptage d'appel interne avec les appels tally().
Il y a encore pas mal de code de test, mais ce code est très strict. S'il vous semble encore terrifiant il l'est bien moins que si nous avions essayé sans les objets fantaisie et ce test en particulier, interactions plutôt que résultat, est toujours plus difficile à mettre en place. Le plus souvent vous aurez besoin de tester des situations plus complexes sans ce niveau ni cette précision. En outre une partie peut être remaniée avec la méthode de scénario de test setUp().
Voici la liste complète des attentes que vous pouvez placer sur un objet fantaisie avec SimpleTest...
| Attente | Nécessite tally() |
|---|---|
expectArguments($method, $args) |
Non |
expectArgumentsAt($timing, $method, $args) |
Non |
expectCallCount($method, $count) |
Oui |
expectMaximumCallCount($method, $count) |
Non |
expectMinimumCallCount($method, $count) |
Oui |
expectNever($method) |
Non |
expectOnce($method, $args) |
Oui |
expectAtLeastOnce($method, $args) |
Oui |
setReturn(). Cet argument est optionnel pour expectOnce() et expectAtLeastOnce().
expectMaximumCallCount() est légèrement différente dans le sens où elle ne pourra générer qu'un échec. Elle reste silencieuse si la limite n'est jamais atteinte.
Comme avec les assertions dans les scénarios de test, toutes ces attentes peuvent accepter une surcharge de message sous la forme d'un paramètre supplémentaire. Par ailleurs le message d'échec original peut être inclus dans le résultat avec "%s".
Il existe trois approches pour créer des objets fantaisie en comprenant celle utilisée par SimpleTest. Les coder à la main en utilisant une classe de base, les générer dans un fichier ou les générer dynamiquement à la volée.
Les objets fantaisie générés avec SimpleTest sont dynamiques. Ils sont créés à l'exécution dans la mémoire, grâce à eval(), plutôt qu'écrits dans un fichier. Cette opération les rend facile à créer, en une seule ligne, surtout par rapport à leur création à la main dans une hiérarchie de classe parallèle. Le problème avec ce comportement tient généralement dans la mise en place des tests proprement dits. Si les objets originaux changent les versions fantaisie sur lesquels reposent les tests, une désynchronisation peut subvenir. Cela peut aussi arriver avec l'approche en hiérarchie parallèle, mais c'est détecté beaucoup plus vite.
Bien sûr, la solution est d'ajouter de véritables tests d'intégration. Vous n'en avez pas besoin de beaucoup et le côté pratique des objets fantaisie fait plus que compenser la petite dose de test supplémentaire. Vous ne pouvez pas avoir confiance dans du code qui ne serait testé que par des objets fantaisie.
Si vous restez déterminé de construire des librairies statiques d'objets fantaisie parce que vous souhaitez émuler un comportement très spécifique, vous pouvez y parvenir grâce au générateur de classe de SimpleTest. Dans votre fichier librairie, par exemple mocks/connection.php pour une connexion à une base de données, créer un objet fantaisie et provoquer l'héritage pour hériter pour surcharger des méthodes spéciales ou ajouter des préréglages...
generate dit au générateur de classe d'en créer une appelée BasicMockConnection plutôt que la plus courante MockConnection. Ensuite nous héritons à partir de celle-ci pour obtenir notre version de MockConnection. En interceptant de cette manière nous pouvons ajouter un comportement, ici transformer la valeur par défaut de query() en "false".
En utilisant le nom par défaut nous garantissons que le générateur de classe fantaisie n'en recréera pas une autre différente si il est invoqué ailleurs dans les tests. Il ne créera jamais de classe si elle existe déjà. Aussi longtemps que le fichier ci-dessus est inclus avant alors tous les tests qui généraient MockConnection devraient utiliser notre version à présent. Par contre si nous avons une erreur dans l'ordre et que la librairie de fantaisie en crée une d'abord alors la création de la classe échouera tout simplement.
Utiliser cette astuce si vous vous trouvez avec beaucoup de comportement en commun sur les objets fantaisie ou si vous avez de fréquents problèmes d'intégration plus tard dans les étapes de test.
Mais au moment d'écrire ces lignes c'est le seul à gérer les objets fantaisie, donc vous êtes bloqué avec lui ?
Non, pas du tout.
SimpleTest est une boîte à outils et parmi ceux-ci on trouve les objets fantaisie qui peuvent être utilisés indépendamment. Supposons que vous avez votre propre testeur unitaire favori et que tous vos tests actuels l'utilisent. Prétendez que vous avez appelé votre tester unitaire PHPUnit (c'est ce que tout le monde a fait) et que la classe principale de test ressemble à...
assertion() réalise, c'est de préparer une sortie embellie alors le paramètre boolien de l'assertion sert à déterminer s'il s'agit d'une erreur ou d'un succès. Supposons qu'elle est utilisée de la manière suivante...
Il y a une méthode protégée sur la classe de base des objets fantaisie : elle s'appelle _assertTrue(). En surchargeant cette méthode nous pouvons utiliser notre propre format d'assertion. Nous commençons avec une sous-classe, dans my_mock.php...
MyMock créera un objet qui parle le même langage que votre testeur. Bien sûr le truc c'est que nous créons jamais un tel objet : le générateur s'en chargera. Nous avons juste besoin d'une ligne de code supplémentaire pour dire au générateur d'utiliser vos nouveaux objets fantaisie...