Des tests plus faciles avec la moquerie

C’est une triste vérité que, si le principe de base des tests est assez simple, il est plus difficile que vous ne l’espériez d’introduire pleinement ce processus dans votre flux de travail quotidien de codage. Le seul jargon utilisé peut être accablant! Heureusement, divers outils vous aident à rendre le processus aussi simple que possible. Mockery, le premier framework d'objets mock pour PHP, est l'un de ces outils.!

Dans cet article, nous allons expliquer en quoi consiste le moquage, pourquoi il est utile et comment intégrer Mockery dans votre flux de travail de test..


Moqueur décodé

Un objet fictif n'est rien de plus qu'un simple jargon de test faisant référence à la simulation du comportement d'objets réels. En termes simples, souvent, lors des tests, vous ne voudrez pas exécuter une méthode particulière. Au lieu de cela, vous devez simplement vous assurer qu’il s’appelle, en fait,.

Peut-être un exemple est en ordre. Imaginez que votre code déclenche une méthode qui enregistre un peu de données dans un fichier. Lors du test de cette logique, vous ne voulez certainement pas toucher physiquement le système de fichiers. Cela pourrait réduire considérablement la vitesse de vos tests. Dans ces situations, il est préférable de simuler votre classe de système de fichiers et, plutôt que de lire manuellement le fichier pour prouver qu'il a été mis à jour, assurez-vous simplement que la méthode applicable à la classe a bien été appelée. C'est moqueur! Il n'y a rien de plus que ça; simuler le comportement des objets.

Rappelez-vous: le jargon est juste du jargon. Ne permettez jamais qu'une terminologie déroutante vous dissuade d'apprendre une nouvelle compétence..

En particulier, à mesure que votre processus de développement évolue - y compris en adoptant le principe de responsabilité unique et en exploitant l'injection de dépendance - une familiarité avec les moqueries deviendra rapidement essentielle..

Mocks vs. Stubs: Les chances sont élevées que vous entendez souvent les termes, moquer et talon, jetés à propos interchangeables. En fait, les deux servent des objectifs différents. La première fait référence au processus de définition des attentes et d’assurance du comportement souhaité. En d'autres termes, une simulation peut potentiellement conduire à un test infructueux. Un talon, par contre, est simplement un ensemble factice de données pouvant être transmises pour répondre à certains critères..

La bibliothèque de tests de defacto pour PHP, PHPUnit, est livrée avec sa propre API pour les objets moqueurs; Malheureusement, travailler avec peut s'avérer fastidieux. Comme vous le savez sûrement, plus les tests sont difficiles, plus il est probable que le développeur ne le fera tout simplement pas (et malheureusement)..

Heureusement, diverses solutions tierces sont disponibles via Packagist (référentiel de paquets de Composer), ce qui permet une lisibilité accrue et, plus important encore,, inscriptibilité. Parmi ces solutions - et la plus remarquable de la série - se trouve Mockery, un framework d'objet fictif agnostique.

Conçu comme une alternative immédiate pour ceux qui sont submergés par la verbosité moqueuse de PHPUnit, Mockery est un utilitaire simple mais puissant. Comme vous le constaterez sûrement, il s'agit du standard de l'industrie pour le développement moderne de PHP..


Installation

Comme la plupart des outils PHP modernes, Mockery peut être installé avec Composer.

Comme la plupart des outils PHP de nos jours, la méthode recommandée pour installer Mockery est d'utiliser Composer (même si elle est également disponible via Pear).

Attends, c'est quoi ce truc de compositeur? C'est l'outil préféré de la communauté PHP pour la gestion des dépendances. Il fournit un moyen simple de déclarer les dépendances d'un projet et de les extraire avec une seule commande. En tant que développeur PHP moderne, vous devez avoir une connaissance de base de Composer et de son utilisation..

Si vous travaillez en même temps, ajoutez un nouveau composer.json déposer dans un projet vide et ajouter:

 "require-dev": "moqueur / moqueur": "dev-master"

Ce bit de JSON spécifie que, pour le développement, votre application nécessite la bibliothèque Mockery. A partir de la ligne de commande, un compositeur installer --dev va tirer dans le paquet.

$ composer install --dev Chargement de référentiels de compositeur avec les informations de paquet. Installation de dépendances (y compris require-dev) - Installation de mockery / mockery (dev-master 5a71299) Clonage du fichier 5a712994e1e3ee604b0d355d1af342172c6f475f Écriture d'un fichier de verrouillage

En prime, Composer est livré gratuitement avec son propre autochargeur! Soit spécifier un classmap de répertoires et composer dump-autoload, ou suivez la norme PSR-0 et ajustez la structure de votre répertoire en conséquence. Reportez-vous à Nettuts + pour en savoir plus. Si vous avez toujours manuellement besoin d'innombrables fichiers dans chaque fichier PHP, eh bien, vous le faites peut-être mal..


Le dilemme

Avant de pouvoir mettre en œuvre une solution, il est préférable de commencer par examiner le problème. Imaginez que vous deviez mettre en place un système pour gérer le processus de génération de contenu et son écriture dans un fichier. Peut-être que le générateur compile diverses données, soit à partir de fichiers de remplacement locaux, soit d'un service Web, puis ces données sont écrites dans le système de fichiers..

Si l'on respecte le principe de responsabilité unique - qui dicte que chaque classe devrait être responsable de exactement une chose - il va de soi que nous devrions scinder cette logique en deux classes: une pour générer le contenu nécessaire et une autre pour écrire physiquement les données dans un fichier. UNE Générateur et Fichier classe, respectivement, devrait faire l'affaire.

Pointe: Pourquoi ne pas utiliser contenu_entrée_fichier directement depuis le Générateur classe? Eh bien, demandez-vous: "Comment pourrais-je tester cela?"Il existe des techniques, telles que le patching de singe, qui peuvent vous permettre de surcharger ce genre de choses, mais il est préférable d'envelopper cette fonctionnalité afin qu'elle puisse facilement être utilisée avec des outils tels que Mockery!

Voici une structure de base (avec une bonne dose de pseudo-code) pour notre Générateur classe.

fichier = $ fichier;  function protégée getContent () // simplifié pour la démo, retourne 'foo bar';  fonction publique fire () $ content = $ this-> getContent (); $ this-> fichier-> put ('foo.txt', $ content); 

Injection de dépendance

Ce code exploite ce que nous appelons l'injection de dépendance. Encore une fois, il s'agit simplement d'un jargon de développeur pour injecter les dépendances d'une classe via sa méthode constructeur, plutôt que de les coder en dur.

Pourquoi est-ce bénéfique? Parce que sinon, nous ne pourrions pas nous moquer de la Fichier classe! Bien sûr, nous pourrions nous moquer de la Fichier classe, mais si son instanciation est codée en dur dans la classe que nous testons, il n’ya pas de moyen facile de remplacer cette instance par la version simulée.

fonction publique __construct () // anti-pattern $ this-> file = new File; 

Le meilleur moyen de créer une application testable consiste à aborder chaque nouvel appel de méthode avec la question "Comment pourrais-je tester cela?"Bien qu'il existe des astuces pour contourner ce codage en dur, cela est généralement considéré comme une mauvaise pratique. Au lieu de cela, injectez toujours les dépendances d'une classe par l'intermédiaire du constructeur, ou via l'injection de setter.

L'injection de poseur est plus ou moins identique à l'injection de constructeur. Le principe est exactement le même. la seule différence est que, plutôt que d'injecter les dépendances de la classe via sa méthode constructeur, elles sont effectuées via une méthode setter, comme suit:

fonction publique setFile (fichier $ fichier) $ ceci-> fichier = $ fichier; 

Une critique courante de l'injection de dépendance est qu'elle introduit une complexité supplémentaire dans une application, dans le but de la rendre plus testable. Selon cet auteur, même si l'argument de la complexité est discutable, vous pouvez, si vous le préférez, autoriser l'injection de dépendance tout en spécifiant des valeurs par défaut. Voici un exemple:

class Generator fonction publique __construct (Fichier $ fichier = null) $ ceci-> fichier = $ fichier?: new Fichier; 

Maintenant, si une instance de Fichier est passé au constructeur, cet objet sera utilisé dans la classe. D'autre part, si rien n'est passé, le Générateur volonté se retirer d'instancier manuellement la classe applicable. Cela permet des variations telles que:

# Class instancie File new Generator; # Inject File new Generator (nouveau fichier); # Injecte une maquette de File pour tester le nouveau générateur ($ mockedFile);

Continuant sur, pour les besoins de ce tutoriel, le Fichier classe sera rien de plus qu'un simple wrapper autour de PHP contenu_entrée_fichier une fonction.

 

Plutôt simple, hein? Écrivons un test pour voir, de première main, quel est le problème.

Feu(); 

Veuillez noter que ces exemples supposent que les classes nécessaires sont automatiquement chargées avec Composer. Votre composer.json Le fichier accepte éventuellement un chargement automatique objet, où vous pouvez spécifier les répertoires ou les classes à charger automatiquement. Plus de saleté exiger des déclarations!

Si je travaille, je cours phpunit retournera:

OK (1 test, 0 assertions)

C'est vert; cela signifie que nous pouvons passer à la tâche suivante, non? Eh bien, pas exactement. En effet, même s’il est vrai que le code fonctionne, chaque fois que ce test est exécuté, une foo.txt Le fichier sera créé sur le système de fichiers. Qu'en est-il lorsque vous avez écrit des dizaines de tests supplémentaires? Comme vous pouvez l'imaginer, très rapidement, la vitesse d'exécution de votre test va bégayer.

Bien que les tests réussissent, ils ne touchent pas correctement le système de fichiers.

Toujours pas convaincu? Si la vitesse réduite des tests ne vous influence pas, considérez le bon sens. Pensez-y: nous testons le Générateur classe; pourquoi avons-nous un intérêt à exécuter du code à partir de la Fichier classe? Il devrait avoir ses propres tests! Pourquoi diable ferions-nous double??


La solution

Espérons que la section précédente fournissait l'illustration parfaite de la raison pour laquelle se moquer est essentiel. Comme il a été noté précédemment, bien que nous puissions utiliser l’API native de PHPUnit pour répondre à nos exigences en matière de moquerie, il n’est pas très agréable de travailler avec. Pour illustrer cette vérité, voici un exemple pour affirmer qu'un objet simulé doit recevoir une méthode, getName et retour John Doe.

fonction publique testNativeMocks () $ mock = $ this-> getMock ('SomeClass'); $ mock-> attend ($ this-> once ()) -> méthode ('getName') -> will ($ this-> returnValue ('John Doe')); 

Bien que le travail soit fait - affirmant qu'un getName La méthode est appelée une fois et retourne John Doe - L'implémentation de PHPUnit est déroutante et verbeuse. Avec Mockery, nous pouvons améliorer considérablement sa lisibilité.

fonction publique testMockery () $ mock = Mockery :: mock ('SomeClass'); $ mock-> shouldReceive ('getName') -> once () -> andReturn ('John Doe'); 

Remarquez comment ce dernier exemple lit (et parle) mieux.

Continuant avec l'exemple de la précédente "Dilemme section, cette fois, au sein de la GeneratorTest classe, simulons plutôt le comportement de la Fichier classe avec moquerie. Voici le code mis à jour:

shouldReceive ('put') -> avec ('foo.txt', 'foo bar') -> once (); $ generateur = nouveau generateur ($ mockedFile); $ générateur-> incendie (); 

Confus par le Mockery :: close () référence dans le abattre méthode? Cet appel statique nettoie le conteneur Mockery utilisé par le test en cours et exécute les tâches de vérification nécessaires à vos attentes..

On peut se moquer d’une classe en utilisant le texte lisible. Mockery :: mock () méthode. Ensuite, vous devrez généralement spécifier les méthodes de cet objet fictif que vous comptez appeler, ainsi que tous les arguments applicables. Ceci peut être accompli, via le shouldReceive (METHOD) et avec (ARG) les méthodes.

Dans ce cas, quand on appelle $ générer-> feu (), nous affirmons qu'il devrait appeler le mettre méthode sur le Fichier par exemple, et l'envoyer le chemin, foo.txt, et les données, foo bar.

// libraries / Generator.php fonction publique fire () $ content = $ this-> getContent (); $ this-> fichier-> put ('foo.txt', $ content); 

Parce que nous utilisons l'injection de dépendance, c'est maintenant un jeu d'enfant d'injecter le faux Fichier objet.

$ generateur = nouveau generateur ($ mockedFile);

Si nous relançons les tests, ils reviendront quand même au vert, cependant, le Fichier classe - et, par conséquent, le système de fichiers - ne sera jamais touché! Encore une fois, il n'y a pas besoin de toucher Fichier. Il devrait avoir ses propres tests! Se moquer de la victoire!

Objets simulés simples

Les objets fictifs ne doivent pas toujours référencer une classe. Si vous n’avez besoin que d’un objet simple, peut-être pour un utilisateur, vous pouvez passer un tableau au moquer method - où, pour chaque élément, la clé et la valeur correspondent respectivement au nom de la méthode et à la valeur renvoyée.

fonction publique testSimpleMocks () $ user = Mockery :: mock (['getFullName' => 'Jeffrey Way']); $ user-> getFullName (); // Jeffrey Way

Valeurs de retour de méthodes simulées

Il y aura sûrement des moments où une méthode de classe fausse devra renvoyer une valeur. Continuant avec notre exemple Générateur / Fichier, que se passe-t-il si nous devons nous assurer que, si le fichier existe déjà, il ne devrait pas être écrasé? Comment pourrions-nous accomplir cela?

La clé est d'utiliser le andReturn () méthode sur votre objet simulé pour simuler différents États. Voici un exemple mis à jour:

fonction publique testDoesNotOverwriteFile () $ mockedFile = Mockery :: mock ('File'); $ mockedFile-> shouldReceive ('existe') -> once () -> andReturn (true); $ mockedFile-> shouldReceive ('put') -> jamais (); $ generateur = nouveau generateur ($ mockedFile); $ générateur-> incendie (); 

Ce code mis à jour affirme maintenant qu’un existe méthode doit être déclenchée sur le raillé Fichier classe, et il devrait, pour les besoins du chemin de ce test, renvoyer vrai, signalant que le fichier existe déjà et ne doit pas être écrasé. Nous veillons ensuite à ce que, dans de telles situations, le mettre méthode sur le Fichier la classe n'est jamais déclenchée. Avec Mockery, c’est facile, grâce au jamais() attente.

$ mockedFile-> shouldReceive ('put') -> jamais ();

Si nous relançons les tests, une erreur sera renvoyée:

La méthode existe () à partir du fichier doit être appelée exactement 1 fois mais 0 fois..

Aha; donc le test prévu que $ this-> fichier-> existe () devrait être appelé, mais cela n'est jamais arrivé. En tant que tel, cela a échoué. Corrigeons-le!

fichier = $ fichier;  function protégée getContent () // simplifié pour la démo, retourne 'foo bar';  fonction publique fire () $ content = $ this-> getContent (); $ file = 'foo.txt'; if (! $ this-> fichier-> existe ($ fichier)) $ ceci-> fichier-> put ($ fichier, $ contenu); 

C'est tout ce qu'on peut en dire! Nous avons non seulement suivi un cycle TDD (développement piloté par les tests), mais les tests sont revenus au vert!

Il est important de se rappeler que ce style de test n’est efficace que si vous testez également les dépendances de votre classe! Sinon, bien que les tests puissent apparaître en vert, le code sera cassé pour la production. Notre démo jusqu’à présent n’a fait que Générateur fonctionne comme prévu. N'oubliez pas de tester Fichier ainsi que!


Attentes

Approfondissons un peu les déclarations d'attentes de Mockery. Vous connaissez déjà devrait recevoir. Soyez prudent avec cela, cependant; son nom est un peu trompeur. Lorsqu'il est laissé à lui-même, il n'est pas nécessaire que la méthode soit déclenchée; la valeur par défaut est zéro ou plusieurs fois (zeroOrMoreTimes ()). Pour affirmer que la méthode doit être appelée une fois, voire plusieurs fois, quelques options sont disponibles:

$ mock-> shouldReceive ('method') -> once (); $ mock-> shouldReceive ('method') -> times (1); $ mock-> shouldReceive ('method') -> atLeast () -> times (1);

Il y aura des moments où des contraintes supplémentaires sont nécessaires. Comme démontré précédemment, cela peut être particulièrement utile lorsque vous devez vous assurer qu'une méthode particulière est déclenchée avec les arguments nécessaires. Il est important de garder à l'esprit que l'attente ne s'appliquera que si une méthode est appelée avec ces arguments exacts.

Voici quelques exemples.

$ mock-> shouldReceive ('get') -> withAnyArgs () -> once (); // valeur par défaut $ mock-> shouldReceive ('get') -> avec ('foo.txt') -> once (); $ mock-> shouldReceive ('put') -> avec ('foo.txt', 'foo bar') -> once ();

Cela peut être étendu encore plus pour permettre aux valeurs d'argument d'être de nature dynamique, tant qu'elles répondent à certains critères. Peut-être souhaitons-nous seulement nous assurer qu'une chaîne est passée à une méthode:

$ mock-> shouldReceive ('get') -> avec (Mockery :: type ('string')) -> once ();

Ou peut-être que l'argument doit correspondre à une expression régulière. Affirmons que tout nom de fichier qui se termine par .SMS devrait être assorti.

$ mockedFile-> shouldReceive ('put') -> avec ('/ \. txt $ /', Mockery :: any ()) -> once ();

Et comme dernier exemple (non limitatif), permettons un tableau de valeurs acceptables, en utilisant le n'importe quel matcher.

$ mockedFile-> shouldReceive ('get') -> avec (Mockery :: anyOf ('log.txt', 'cache.txt')) -> once ();

Avec ce code, l’attente ne s’appliquera que si le premier argument de la obtenir la méthode est log.txt ou cache.txt. Sinon, une exception de moquerie sera levée lors de l'exécution des tests.

Mockery \ Exception \ NoMatchingExpectationException: aucun gestionnaire correspondant n'a été trouvé… 

Pointe: N'oubliez pas, vous pouvez toujours alias Moquerie comme m au sommet de votre classe pour rendre les choses un peu plus succinctes: utilisez Mockery comme m;. Cela permet de plus succinct, m :: mock ().

Enfin, nous avons diverses options pour spécifier ce que la méthode simulée doit faire ou renvoyer. Peut-être n’en avons-nous besoin que pour retourner un booléen. Facile:

$ mock-> shouldReceive ('method') -> once () -> andReturn (false);

Mocks partiels

Vous constaterez peut-être qu'il existe des situations dans lesquelles vous n'avez besoin que de vous moquer d'une seule méthode, plutôt que de l'objet entier. Imaginons, aux fins de cet exemple, qu’une méthode de votre classe fasse référence à une fonction globale personnalisée (gasp) pour extraire une valeur d’un fichier de configuration..

getOption ('timeout'); // faire quelque chose avec $ timeout

Bien qu'il existe quelques techniques différentes pour se moquer des fonctions globales. Néanmoins, il est préférable d'éviter cette méthode d'appeler tous ensemble. C’est précisément à ce moment que des simulacres partiels entrent en jeu.

fonction publique testPartialMockExample () $ mock = Mockery :: mock ('MyClass [getOption]'); $ mock-> shouldReceive ('getOption') -> once () -> andReturn (10000); $ simulacre-> feu (); 

Remarquez comment nous avons placé la méthode à simuler entre crochets. Si vous avez plusieurs méthodes, séparez-les simplement par une virgule, comme suit:

$ mock = Mockery :: mock ('MyClass [méthode1, méthode2]');

Avec cette technique, le reste des méthodes de l'objet se déclenchera et se comportera comme il le ferait normalement. Gardez à l'esprit que vous devez toujours déclarer le comportement de vos méthodes simulées, comme nous l'avons fait ci-dessus. Dans ce cas, quand getOption est appelé, plutôt que d'exécuter le code qu'il contient, nous retournons simplement 10000.

Une autre option consiste à utiliser des simulations partielles passives, que vous pouvez considérer comme définissant un état par défaut pour l'objet simulé: toutes les méthodes sont reportées à la classe parente principale, à moins qu'une attente ne soit spécifiée..

L'extrait de code précédent peut être réécrit comme suit:

fonction publique testPassiveMockExample () $ mock = Mockery :: mock ('MyClass') -> makePartial (); $ mock-> shouldReceive ('getOption') -> once () -> andReturn (10000); $ simulacre-> feu (); 

Dans cet exemple, toutes les méthodes sur Ma classe se comporteront comme ils le feraient normalement, à l'exclusion getOption, qui sera raillé et retournera 10000 '.


Hamcrest

La bibliothèque Hamcrest fournit un ensemble supplémentaire de correspondants pour définir les attentes..

Une fois que vous vous êtes familiarisé avec l'API Mockery, il est recommandé d'utiliser également la bibliothèque Hamcrest, qui fournit un ensemble supplémentaire de correspondants pour définir les attentes lisibles. Comme Mockery, il peut être installé via Composer.

"require-dev": "moquerie / moquerie": "dev-master", "davedevelopment / hamcrest-php": "dev-master"

Une fois installé, vous pouvez utiliser une notation plus lisible par l’homme pour définir vos tests. Vous trouverez ci-dessous quelques exemples, y compris de légères variations permettant d'atteindre le même résultat final..

 

Remarquez comment Hamcrest vous permet d’écrire vos assertions de manière lisible ou concise à votre guise. L'utilisation de la est() fonction n'est rien de plus que le sucre syntaxique pour aider à la lisibilité.

Vous constaterez que Mockery se marie assez bien avec Hamcrest. Par exemple, avec Mockery uniquement, pour spécifier qu’une méthode fictive doit être appelée avec un seul argument de type, chaîne, vous pourriez écrire:

$ mock-> shouldReceive ('method') -> avec (Mockery :: type ('string')) -> once ();

Si vous utilisez Hamcrest, Mockery :: type peut être remplacé par valeur de chaîne(), ainsi:

$ mock-> shouldReceive ('method') -> avec (stringValue ()) -> once ();

Hamcrest suit le RessourceConvention de dénomination des valeurs pour faire correspondre le type d'une valeur.

  • nullValue
  • integerValue
  • tableauValeur
  • rincer et répéter

Alternativement, pour faire correspondre n'importe quel argument, Mockery :: any () pourrait devenir n'importe quoi().

$ file-> shouldReceive ('put') -> avec ('foo.txt', n'importe quoi ()) -> once ();

Résumé

Ironiquement, le plus gros obstacle à l’utilisation de Mockery n’est pas l’API elle-même..

Ironiquement, le plus gros obstacle à l’utilisation de Mockery n’est pas l’API elle-même, mais la compréhension de la raison et du moment de l’utilisation de moquettes dans vos tests.

L'essentiel est d'apprendre et de respecter le principe de responsabilité unique dans votre flux de travail de codage. Inventé par Bob Martin, le SRP dit qu'une classe "devrait avoir une et une seule raison de changer."En d’autres termes, une classe ne devrait pas avoir besoin d’être mise à jour en réponse à plusieurs modifications non liées à votre application, telles que la modification de la logique métier, le formatage de la sortie ou la persistance des données. Dans sa forme la plus simple, comme une méthode, une classe devrait faire une chose.

le Fichier La classe gère les interactions du système de fichiers. UNE MysqlDb le référentiel conserve les données. Un Email La classe prépare et envoie des courriels. Remarquez comment, dans aucun de ces exemples, le mot, et, utilisé.

Une fois que cela est compris, le test devient considérablement plus facile. L’injection de dépendance doit être utilisée pour toutes les opérations ne relevant pas de la classe. parapluie. Lors des tests, concentrez-vous sur une classe à la fois et simulez toutes ses dépendances. De toute façon, vous n'êtes pas intéressé par les tester; ils ont leurs propres tests!

Bien que rien ne vous empêche d'utiliser l'implémentation moqueuse native de PHPUnit, pourquoi s'inquiéter lorsque la lisibilité améliorée de Mockery n'est qu'un mise à jour du compositeur une façon?