Tout sur les moqueries avec PHPUnit

Il existe deux styles de test: les styles "boîte noire" et "boîte blanche". Le test des boîtes noires se concentre sur l'état de l'objet; tandis que les tests de boîte blanche portent sur le comportement. Les deux styles se complètent et peuvent être combinés pour tester minutieusement le code. Railleur nous permet de tester le comportement, et ce tutoriel combine le concept moqueur avec TDD pour construire un exemple de classe qui utilise plusieurs autres composants pour atteindre son objectif.


Étape 1: Introduction aux tests de comportement

Les objets sont des entités qui s'envoient des messages. Chaque objet reconnaît un ensemble de messages auquel il répond. Ceux-ci sont Publique méthodes sur un objet. Privé les méthodes sont l'exact opposé. Ils sont complètement internes à un objet et ne peuvent communiquer avec rien en dehors de celui-ci. Si les méthodes publiques s'apparentent à des messages, les méthodes privées ressemblent à des pensées.

L'ensemble des méthodes, publiques et privées, accessibles via des méthodes publiques représente le comportement d'un objet. Par exemple, dire à un objet de bouge toi permet à cet objet d’interagir non seulement avec ses méthodes internes, mais également avec d’autres objets. Du point de vue de l'utilisateur, l'objet n'a qu'un comportement simple: il se déplace.

Du point de vue du programmeur, cependant, l’objet doit faire beaucoup de petites choses pour réaliser le mouvement..

Par exemple, imaginons que notre objet est une voiture. Pour qu'il puisse bouge toi, il doit avoir un moteur en marche, être en première vitesse (ou en marche arrière) et les roues doivent tourner. C’est un comportement que nous devons tester et développer pour concevoir et écrire notre code de production..


Étape 2: Voiture jouet télécommandée

Notre classe testée n'utilise jamais réellement ces objets factices.

Imaginons que nous construisons un programme de contrôle à distance d'une petite voiture. Toutes les commandes de notre classe passent par la télécommande. Nous devons créer une classe qui comprend ce que la télécommande envoie et émet commandes à la voiture.

Ce sera une application d’exercice, et nous supposons que les autres classes contrôlant les différentes parties de la voiture sont déjà écrites. Nous connaissons la signature exacte de toutes ces classes, mais malheureusement, le constructeur automobile n’a pu nous envoyer de prototype, pas même le code source. Tout ce que nous savons, ce sont les noms des classes, leurs méthodes et le comportement que chaque méthode encapsule. Les valeurs de retour sont également spécifiées.


Étape 3: Schéma d'application

Voici le schéma complet de l'application. Il n'y a pas d'explication à ce stade; tout simplement garder à l'esprit pour référence ultérieure.


Étape 4: Testez les doubles

Un stub de test est un objet permettant de contrôler l'entrée indirecte du code testé.

Mocking est un style de test qui nécessite son propre ensemble d'outils, un ensemble d'objets spéciaux représentant différents niveaux de simulation du comportement d'un objet. Ceux-ci sont:

  • objets factices
  • bouts d'essai
  • espions de test
  • moquettes d'essai
  • tester des faux

Chacun de ces objets a sa portée et son comportement. Dans PHPUnit, ils sont créés avec le $ this-> getMock () méthode. La différence est de savoir comment et pour quelles raisons les objets sont utilisés.

Pour mieux comprendre ces objets, je vais implémenter le "Contrôleur de voiture miniature" étape par étape en utilisant les types d'objets, dans l'ordre indiqué ci-dessus. Chaque objet de la liste est plus complexe que l'objet précédent. Cela conduit à une mise en œuvre radicalement différente de celle du monde réel. De plus, étant une application imaginaire, j'utiliserai des scénarios qui pourraient même ne pas être réalisables dans une vraie voiture miniature. Mais bon, imaginons ce dont nous avons besoin pour comprendre la situation dans son ensemble.


Étape 5: Objet factice

Les objets factices sont des objets dont dépend le système sous test (SUT), mais ils ne sont en réalité jamais utilisés. Un objet factice peut être un argument passé à un autre objet, ou il peut être renvoyé par un deuxième objet puis passé à un troisième objet. Le fait est que notre classe testée n'utilise jamais réellement ces objets factices. En même temps, l'objet doit ressembler à un objet réel; sinon, le destinataire peut le refuser.

Le meilleur moyen d’illustrer cela est d’imaginer un scénario; dont le schéma est ci-dessous:

L'objet orange est le RemoteControlTranslator. Son objectif principal est de recevoir les signaux de la télécommande et de les traduire en messages pour nos classes. À un moment donné, l’utilisateur fera une "Prêt à partir" action sur la télécommande. Le traducteur recevra le message et créera les cours nécessaires pour que la voiture soit prête à partir..

Le fabricant a dit que "Prêt à partir" signifie que le moteur est démarré, que la boîte de vitesses est au point mort et que les feux sont allumés ou éteints selon la demande de l'utilisateur.

Cela signifie que l'utilisateur peut prédéfinir l'état des voyants avant d'être prêt à fonctionner. Ils s'allument ou s'éteignent en fonction de cette valeur prédéfinie lors de l'activation.. RemoteControlTranslator envoie ensuite toutes les informations nécessaires au CarControl classe' getReadyToGo ($ moteur, $ boîte de vitesses, $ électronique, $ lumières) méthode. Je sais que cette conception est loin d’être parfaite et qu’elle enfreint quelques principes et modèles, mais c’est très bien pour cet exemple..

Commencez notre projet avec cette structure de fichier initiale:

Rappelez-vous que toutes les classes du CarInterface dossier sont fournis par le fabricant de la voiture; nous ne connaissons pas leur implémentation. Nous ne connaissons que les signatures de classe, mais nous ne nous en soucions pas pour le moment..

Notre objectif principal est de mettre en œuvre le CarController classe. Afin de tester cette classe, nous devons imaginer comment nous voulons l’utiliser. En d’autres termes, nous nous mettons à la place du RemoteControlTranslator et / ou toute autre classe future pouvant utiliser CarController. Commençons par créer le cas pour notre classe.

La classe CarControllerTest étend PHPUnit_Framework_TestCase 

Puis ajoutez une méthode de test.

 fonction testItCanGetReadyTheCar () 

Maintenant, réfléchissons à ce que nous devons passer au getReadyToGo () méthode: un moteur, une boîte de vitesses, un contrôleur électronique et des informations lumineuses. Pour cet exemple, nous nous moquerons des lumières:

require_once '… /CarController.php'; include '… /autoloadCarInterfaces.php'; La classe CarControllerTest étend PHPUnit_Framework_TestCase function testItCanGetReadyTheCar () $ carController = new CarController (); $ engine = new Engine (); $ gearbox = new Gearbox (); $ electornics = new Electronics (); $ dummyLights = $ this-> getMock ('Lights'); $ this-> assertTrue ($ carController-> getReadyToGo ($ engine, $ gearbox, $ electornics, $ dummyLights)); 

Cela va évidemment échouer avec:

Erreur irrécupérable PHP: Appel de la méthode non définie CarController :: getReadyToGo ()

Malgré l’échec, ce test nous a fourni un point de départ pour notre CarController la mise en oeuvre. J'ai inclus un fichier, appelé autoloadCarInterfaces.php, ce n'était pas sur la liste initiale. J'ai réalisé que j'avais besoin de quelque chose pour charger les classes et j'ai écrit une solution très basique. Nous pouvons toujours le réécrire lorsque les vraies classes sont fournies, mais c'est une tout autre histoire. Pour le moment, nous allons nous en tenir à la solution simple:

foreach (scandir (dirname (__ FILE__). '/ CarInterface') en tant que $ nom de fichier) $ path = dirname (__ FILE__). '/ CarInterface /'. $ nom de fichier; if (is_file ($ path)) require_once $ path; 

Je présume que ce chargeur de classe est évident pour tout le monde; alors, discutons du code de test.

Tout d'abord, nous créons une instance de CarController, la classe que nous voulons tester. Ensuite, nous créons des instances de toutes les autres classes qui nous intéressent: moteur, boîte de vitesses et électronique..

Nous créons ensuite un mannequin Lumières object en appelant PHPUnit getMock () méthode et en passant le nom du Lumières classe. Ceci retourne une instance de Lumières, mais chaque méthode retourne nul--un objet factice. Cet objet factice ne peut rien faire, mais il donne à notre code l'interface nécessaire pour travailler avec Lumière objets.

Il est très important de noter que $ dummyLights est un Lumières objet, et tout utilisateur qui attend un Lumière objet peut utiliser l'objet factice sans savoir que ce n'est pas un réel Lumières objet.

Pour éviter toute confusion, je vous recommande de spécifier le type d'un paramètre lors de la définition d'une fonction. Cela oblige le runtime PHP à vérifier vérifier les arguments passés à une fonction. Sans spécifier le type de données, vous pouvez transmettre n'importe quel objet à n'importe quel paramètre, ce qui peut entraîner l'échec de votre code. Dans cet esprit, examinons la Électronique classe:

require_once 'Lights.php'; classe Electronics function turnOn (Lumières $ lumières) 

Implémentons un test:

class CarController function getReadyToGo (Moteur $ moteur, Boîte de vitesses $ Boîte de vitesses, Électronique $ électronique, Lumières $ lumières) $ moteur-> start (); $ gearbox-> shift ('N'); $ electronics-> turnOn ($ lumières); retourne vrai; 

Comme vous pouvez le voir, le getReadyToGo () fonction utilisée le $ lumières objet dans le seul but de l'envoyer à la $ électronique objets allumer() méthode. Est-ce la solution idéale pour une telle situation? Probablement pas, mais vous pouvez clairement voir comment un objet factice, sans aucun rapport avec le getReadyToGo () fonction, est transmis à l'objet qui en a vraiment besoin.

Veuillez noter que toutes les classes contenues dans le CarInterface répertoire fournit des objets factices lors de l’initialisation. Supposons également que, pour cet exercice, nous attendons du fabricant qu'il fournisse les classes réelles dans le futur. Nous ne pouvons pas compter sur leur manque actuel de fonctionnalités; oui, nous devons nous assurer que nos tests réussissent.


Étape 6: "Stub" le statut et aller de l'avant

Un stub de test est un objet permettant de contrôler l'entrée indirecte du code testé. Mais qu'est-ce que l'entrée indirecte? C'est une source d'information qui ne peut pas être spécifiée directement.

L'exemple le plus courant d'un talon de test est lorsqu'un objet demande des informations à un autre objet puis fait quelque chose avec ces données..

Les espions, par définition, sont des bouts plus capables.

Les données ne peuvent être obtenues qu'en demandant à un objet spécifique, et dans de nombreux cas, ces objets sont utilisés à des fins spécifiques dans la classe testée. Nous ne voulons pas "new up" (nouveau SomeClass ()) une classe dans une autre classe à des fins de test. Par conséquent, nous devons injecter une instance d'une classe qui agit comme Une classe sans injecter un réel Une classe objet.

Ce que nous voulons, c'est une classe de bout, ce qui conduit ensuite à injection de dépendance. L'injection de dépendance (ID) est une technique qui consiste à injecter un objet dans un autre objet, le forçant à utiliser l'objet injecté. La DI est courante dans le TDD et elle est absolument requise dans presque tous les projets. Il fournit un moyen simple de forcer un objet à utiliser une classe préparée pour le test au lieu d'une classe réelle utilisée dans l'environnement de production..

Faisons avancer notre petite voiture.

Nous voulons implémenter une méthode appelée Avance(). Cette méthode interroge d'abord un StatusPanel objet pour l'état du carburant et du moteur. Si la voiture est prête à partir, la méthode demande à l'électronique d'accélérer.

Pour mieux comprendre le fonctionnement d'un stub, je vais d'abord écrire le code pour la vérification de l'état et l'accélération:

 fonction goForward (Electronics $ electronics) $ statusPanel = new StatusPanel (); if ($ statusPanel-> engineIsRunning () && $ statusPanel-> là-basFuel ()) $ electronics-> accelerate (); 

Ce code est assez simple, mais nous n’avons pas de véritable moteur ou carburant pour tester notre aller de l'avant() la mise en oeuvre. Notre code ne sera même pas entrer dans le si déclaration parce que nous n'avons pas StatusPanel classe. Mais si nous continuons avec le test, une solution logique commence à émerger:

 function testItCanAccelerate () $ carController = new CarController (); $ electronics = new Electronics (); $ stubStatusPanel = $ this-> getMock ('StatusPanel'); $ stubStatusPanel-> attend ($ this-> any ()) -> méthode ('thereIsEnoughFuel') -> will ($ this-> returnValue (TRUE)); $ stubStatusPanel-> attend ($ this-> any ()) -> méthode ('engineIsRunning') -> will ($ this-> returnValue (TRUE)); $ carController-> goForward ($ electronics, $ stubStatusPanel); 

Explication ligne par ligne:

J'aime la récursion; il est toujours plus facile de tester la récursion que les boucles.

  • créer un nouveau CarController
  • créer la personne à charge Électronique objet
  • créer une maquette pour le StatusPanel
  • attendez-vous à appeler il y a suffisamment de carburant () zéro ou plusieurs fois et retour vrai
  • attendez-vous à appeler engineIsRunning () zéro ou plusieurs fois et retour vrai
  • appel aller de l'avant() avec Électronique et StubbedStatusPanel objet

C’est le test que nous voulons écrire, mais cela ne fonctionnera pas avec notre implémentation actuelle de aller de l'avant(). Nous devons le modifier:

 fonction goForward (Electronique $ electronics, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : new StatusPanel (); if ($ statusPanel-> engineIsRunning () && $ statusPanel-> là-basFuel ()) $ electronics-> accelerate (); 

Notre modification utilise injection de dépendance en ajoutant un second paramètre optionnel de type StatusPanel. Nous déterminons si ce paramètre a une valeur et créons un nouveau StatusPanel si $ statusPanel est nul. Cela garantit qu'un nouveau StatusPanel l'objet est créé en production tout en nous permettant de tester la méthode.

Il est important de spécifier le type de $ statusPanel paramètre. Cela garantit que seul un StatusPanel object (ou un objet d'une classe héritée) peut être passé à la méthode. Mais même avec cette modification, notre test n'est toujours pas complet.


Étape 7: Terminez le test avec une vraie maquette de test

Nous devons tester une maquette Électronique objet pour assurer notre méthode d'appels de l'étape 6 accélérer(). Nous ne pouvons pas utiliser le réel Électronique classe pour plusieurs raisons:

  • Nous n'avons pas la classe.
  • Nous ne pouvons pas vérifier son comportement.
  • Même si nous pouvions l'appeler, nous devrions le tester isolément.

Un modèle de test est un objet capable de contrôler à la fois les entrées et les sorties indirectes et dispose d'un mécanisme permettant une assertion automatique des attentes et des résultats. Cette définition peut paraître un peu déroutante, mais elle est vraiment très simple à mettre en œuvre:

 function testItCanAccelerate () $ carController = new CarController (); $ electronics = $ this-> getMock ('Electronics'); $ electronics-> attend ($ this-> once ()) -> méthode ('accélérer'); $ stubStatusPanel = $ this-> getMock ('StatusPanel'); $ stubStatusPanel-> attend ($ this-> any ()) -> méthode ('thereIsEnoughFuel') -> will ($ this-> returnValue (TRUE)); $ stubStatusPanel-> attend ($ this-> any ()) -> méthode ('engineIsRunning') -> will ($ this-> returnValue (TRUE)); $ carController-> goForward ($ electronics, $ stubStatusPanel); 

Nous avons simplement changé le $ électronique variable. Au lieu de créer un réel Électronique objet, on se moque simplement d'un.

Sur la ligne suivante, nous définissons une attente sur la $ électronique objet. Plus précisément, nous attendons que le accélérer() La méthode est appelée une seule fois ($ this-> once ()). Le test passe maintenant!

N'hésitez pas à jouer avec ce test. Essayez de changer $ this-> once () dans $ this-> exactement (2) et voyez quel beau message d'échec PHPUnit vous donne:

1) CarControllerTest :: testItCanAccelerate L'attente a échoué car le nom de la méthode est égal à ; quand invoqué 2 fois. La méthode devait être appelée 2 fois, voire 1 fois..

Étape 8: Utilisez un test d'espion

Un espion de test est un objet capable de capturer une sortie indirecte et de fournir une entrée indirecte si nécessaire.

La sortie indirecte est quelque chose que nous ne pouvons pas observer directement. Par exemple: lorsque la classe testée calcule une valeur puis l'utilise comme argument pour la méthode d'un autre objet. La seule façon d'observer cette sortie est de demander à l'objet appelé la variable utilisée pour accéder à sa méthode..

Cette définition fait un espion presque une maquette.

La principale différence entre un simulacre et un espion réside dans le fait que les faux spécimens ont des assertions et des attentes intégrées..

Dans ce cas, comment pouvons-nous créer un test d’espion avec PHPUnit? getMock ()? Nous ne pouvons pas (enfin, nous ne pouvons pas créer un pur espion), mais nous pouvons créer des simulacres capables d'espionner d'autres objets.

Mettons en place le système de freinage pour pouvoir arrêter la voiture. Le freinage est très simple. la télécommande détectera l'intensité de freinage de l'utilisateur et l'enverra au contrôleur. La télécommande fournit également un "arrêt d'urgence!" bouton. Cela doit engager instantanément les freins avec une puissance maximale.

La puissance de freinage mesure des valeurs allant de 0 à 100, 0 correspondant à rien et 100 correspondant à la puissance de freinage maximale. L'arrêt d'urgence! commande sera reçu comme appel différent.

le CarController enverra un message au Électronique objet pour activer le système de freinage. Le contrôleur de voiture peut également interroger le StatusPanel pour les informations de vitesse obtenues par des capteurs sur la voiture.

Mise en oeuvre à l'aide d'un espion de test pur

Commençons par implémenter un objet espion pur sans utiliser l'infrastructure moqueuse de PHPUnit. Cela vous donnera une meilleure compréhension du concept d'espion de test. Nous commençons par vérifier la Électronique signature de l'objet.

classe Electronics function turnOn (Lumières allumées)  function accelerate ()  function pushBrakes ($ brakingPower) 

Nous sommes intéressés par le PushBrakes () méthode. Je ne l'ai pas appelé frein() pour éviter toute confusion avec le Pause mot clé en PHP.

Pour créer un véritable espion, nous étendrons Électronique et remplacer le PushBrakes () méthode. Cette méthode surchargée ne pousse pas le frein; au lieu de cela, il enregistrera uniquement la puissance de freinage.

classe SpyingElectronics étend Electronics private $ brakingPower; fonction pushBrakes ($ frekingPower) $ this-> frekingPower = $ frekingPower;  function getBrakingPower () return $ this-> frekingPower; 

Le le getBrakingPower () méthode nous donne la possibilité de vérifier la puissance de freinage dans notre test. Ce n'est pas une méthode que nous utiliserions en production.

Nous pouvons maintenant écrire un test capable de tester la puissance de freinage. Suivant les principes de TDD, nous allons commencer par le test le plus simple et fournir la mise en œuvre la plus élémentaire:

 fonction testItCanStop () $ halfBrakingPower = 50; $ electronicsSpy = nouvelle SpyingElectronics (); $ carController = new CarController (); $ carController-> pushBrakes ($ halfBrakingPower, $ electronicsSpy); $ this-> assertEquals ($ halfBrakingPower, $ electronicsSpy-> getBrakingPower ()); 

Ce test échoue car nous n'avons pas encore de PushBrakes () méthode sur CarController. Corrigeons cela et écrivons-en un:

 fonction pushBrakes ($ freinagePower, électronique $ électronique) $ electronics-> pushBrakes ($ frekingPower); 

Le test réussit maintenant, testant efficacement le PushBrakes () méthode.

Nous pouvons également espionner les appels de méthodes. Tester le StatusPanel la classe est la prochaine étape logique. Il fournit à l'utilisateur différentes informations concernant la voiture télécommandée. Écrivons un test qui vérifie si le StatusPanel l'objet est demandé à propos de la vitesse de la voiture. Nous allons créer un espion pour cela:

La classe SpyingStatusPanel étend StatusPanel private $ speedWasRequested = false; fonction getSpeed ​​() $ this-> speedWasRequested = true;  function speedWasRequested () return $ this-> speedWasRequested; 

Ensuite, nous modifions notre test pour utiliser l'espion:

 fonction testItCanStop () $ halfBrakingPower = 50; $ electronicsSpy = nouvelle SpyingElectronics (); $ statusPanelSpy = new SpyingStatusPanel (); $ carController = new CarController (); $ carController-> pushBrakes ($ halfBrakingPower, $ electronicsSpy, $ statusPanelSpy); $ this-> assertEquals ($ halfBrakingPower, $ electronicsSpy-> getBrakingPower ()); $ this-> assertTrue ($ statusPanelSpy-> speedWasRequested ()); 

Notez que je n'ai pas écrit de test séparé.

La recommandation "une assertion par test" est bonne à suivre, mais lorsque votre test décrit une action nécessitant plusieurs étapes ou états, l'utilisation de plusieurs assertions dans le même test est acceptable..

De plus, cela maintient vos affirmations sur un seul concept au même endroit. Cela permet d’éliminer les doublons de code en ne vous obligeant pas à définir de manière répétée les mêmes conditions pour votre SUT..

Et maintenant la mise en place:

 fonction pushBrakes ($ brakingPower, Electronique $ Electronics, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : new StatusPanel (); $ electronics-> PushBrakes ($ frekingPower); $ statusPanel-> getSpeed ​​(); 

Il y a juste une petite, petite chose qui me dérange: le nom de ce test est testItCanStop (). Cela implique clairement que nous serrions les freins jusqu'à l'arrêt complet de la voiture. Nous avons cependant appelé la méthode PushBrakes (), ce qui n'est pas tout à fait correct. Temps de refactoring:

 fonction stop ($ brakingPower, Electronique $ electronics, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : new StatusPanel (); $ electronics-> PushBrakes ($ frekingPower); $ statusPanel-> getSpeed ​​(); 

N'oubliez pas de changer l'appel de méthode dans le test également.

$ carController-> stop ($ halfBrakingPower, $ electronicsSpy, $ statusPanelSpy);

La sortie indirecte est quelque chose que nous ne pouvons pas observer directement.

À ce stade, nous devons réfléchir à notre système de freinage et à son fonctionnement. Il existe plusieurs possibilités, mais pour cet exemple, supposons que le fournisseur de la voiture miniature ait spécifié que le freinage se produit à des intervalles discrets. Appeler un Électronique objets pushBreakes () La méthode pousse le frein pendant un laps de temps discret, puis le relâche. L'intervalle de temps n'a pas d'importance pour nous, mais imaginons qu'il ne dure qu'une fraction de seconde. Avec un si petit intervalle de temps, nous devons continuellement envoyer PushBrakes () commandes jusqu'à ce que la vitesse soit nulle.

Les espions, par définition, sont plus efficaces et ils peuvent également contrôler les entrées indirectes si nécessaire. Faisons de notre StatusPanel espionner plus capable et offrir une certaine valeur pour la vitesse. Je pense que le premier appel devrait fournir une vitesse positive - disons la valeur de 1. Le deuxième appel fournira la vitesse de 0.

La classe SpyingStatusPanel étend StatusPanel private $ speedWasRequested = false; private $ currentSpeed ​​= 1; fonction getSpeed ​​() if ($ this-> speedWasRequested) $ this-> currentSpeed ​​= 0; $ this-> speedWasRequested = true; return $ this-> currentSpeed;  function speedWasRequested () return $ this-> speedWasRequested;  function spyOnSpeed ​​() return $ this-> currentSpeed; 

Le dépassé getSpeed ​​() La méthode retourne la valeur de vitesse appropriée via le spyOnSpeed ​​() méthode. Ajoutons une troisième affirmation à notre test:

 fonction testItCanStop () $ halfBrakingPower = 50; $ electronicsSpy = nouvelle SpyingElectronics (); $ statusPanelSpy = new SpyingStatusPanel (); $ carController = new CarController (); $ carController-> stop ($ halfBrakingPower, $ electronicsSpy, $ statusPanelSpy); $ this-> assertEquals ($ halfBrakingPower, $ electronicsSpy-> getBrakingPower ()); $ this-> assertTrue ($ statusPanelSpy-> speedWasRequested ()); $ this-> assertEquals (0, $ statusPanelSpy-> spyOnSpeed ​​()); 

Selon la dernière affirmation, la vitesse devrait avoir une valeur de vitesse de 0 après le Arrêtez() méthode termine l'exécution. L'exécution de ce test avec notre code de production aboutit à un échec avec un message crypté:

1) CarControllerTest :: testItCanStop Échec de l'affirmation que 1 correspond à 0.

Ajoutons notre propre message d'assertion personnalisé:

$ this-> assertEquals (0, $ statusPanelSpy-> spyOnSpeed ​​(), "La vitesse attendue est 0 (zéro) après l’arrêt, mais en réalité". $ statusPanelSpy-> spyOnSpeed ​​());

Cela produit un message d'échec beaucoup plus lisible:

1) CarControllerTest :: testItCanStop La vitesse attendue est 0 (zéro) après l’arrêt, mais il s’agissait bien de 1 Échec en affirmant que 1 correspondances attendues 0.

Assez d'échecs! Passons le.

 fonction stop ($ brakingPower, Electronique $ electronics, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : new StatusPanel (); $ electronics-> PushBrakes ($ frekingPower); if ($ statusPanel-> getSpeed ​​()) $ this-> stop ($ brakingPower, $ electronics, $ statusPanel); 

J'aime la récursion; il est toujours plus facile de tester la récursivité que les boucles. Des tests plus faciles signifient un code plus simple, ce qui signifie un meilleur algorithme. Consultez le site prioritaire de la transformation pour plus d'informations à ce sujet..

Retour au framework moqueur de PHPUnit

Assez avec les cours supplémentaires. Réécrivons cela en utilisant le framework moqueur de PHPUnit et éliminons ces espions purs. Pourquoi?

Parce que PHPUnit offre une syntaxe de moquage plus simple et plus efficace, moins de code et de belles méthodes prédéfinies.

Je crée habituellement des espions et des talons purs uniquement lorsque je les moque getMock () serait trop compliqué. Si vos cours sont si complexes que getMock () vous ne pouvez pas les gérer, alors vous avez un problème avec votre code de production - pas avec vos tests.

 fonction testItCanStop () $ halfBrakingPower = 50; $ electronicsSpy = $ this-> getMock ('Electronics'); $ electronicsSpy-> attend ($ this-> exactement (2)) -> méthode ('pushBrakes') -> avec ($ halfBrakingPower); $ statusPanelSpy = $ this-> getMock ('StatusPanel'); $ statusPanelSpy-> attend ($ this-> at (0)) -> méthode ('getSpeed') -> will ($ this-> returnValue (1)); $ statusPanelSpy-> attend ($ this-> at (1)) -> méthode ('getSpeed') -> will ($ this-> returnValue (0)); $ carController = new CarController (); $ carController-> stop ($ halfBrakingPower, $ electronicsSpy, $ statusPanelSpy); 

L'ensemble de toutes les méthodes, publiques et privées, accessibles par l'intermédiaire de méthodes publiques représente le comportement d'un objet..

Une explication ligne par ligne du code ci-dessus:

  • régler la moitié de la puissance de freinage = 50
  • créé un Électronique moquer
  • attendre la méthode PushBrakes () exécuter exactement deux fois avec la puissance de freinage indiquée ci-dessus
  • créer un StatusPanel moquer
  • revenir 1 le premier getSpeed ​​() appel
  • revenir 0 à la seconde getSpeed ​​() exécution
  • appeler le testé Arrêtez() méthode sur un vrai CarController objet

La chose la plus intéressante dans ce code est probablement la $ this-> at ($ someValue) méthode. PHPUnit compte le nombre d'appels à cette maquette. Le comptage se passe au niveau simulé; donc, appelant plusieurs méthodes sur $ statusPanelSpy incrémenterait le compteur. Cela peut sembler un peu contre-intuitif au début; alors regardons un exemple.

Supposons que nous voulions vérifier le niveau de carburant à chaque appel. Arrêtez(). Le code ressemblerait à ceci:

 fonction stop ($ brakingPower, Electronique $ electronics, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : new StatusPanel (); $ electronics-> PushBrakes ($ frekingPower); $ statusPanel-> ThereIsEnoughFuel (); if ($ statusPanel-> getSpeed ​​()) $ this-> stop ($ brakingPower, $ electronics, $ statusPanel); 

Cela va casser notre test. Vous pouvez être confus pourquoi, mais vous obtiendrez le message suivant:

1) CarControllerTest :: testItCanStop L'attente a échoué car le nom de la méthode est égal à  quand invoqué 2 fois. La méthode devait être appelée 2 fois, voire 1 fois..

Il est assez évident que PushBrakes () devrait être appelé deux fois. Pourquoi alors recevons-nous ce message? En raison de l $ this-> at ($ someValue) attente. Le compteur s’incrémente comme suit:

  • premier appel à Arrêtez() -> premier appel à ilIsEnougFuel () => compteur interne à 0
  • premier appel à Arrêtez() -> premier appel à getSpee