Alles over bespotten met PHPUnit

Er zijn twee testtypen: 'black box'- en' white box'-stijlen. Black Box testen richt zich op de staat van het object; overwegende dat testen met witte kaders gericht is op gedrag. De twee stijlen vullen elkaar aan en kunnen worden gecombineerd om code grondig te testen. mocking stelt ons in staat gedrag te testen, en deze tutorial combineert het spotconcept met TDD om een ​​voorbeeldklasse te bouwen die verschillende andere componenten gebruikt om zijn doel te bereiken.


Stap 1: Inleiding tot gedragstesten

Objecten zijn entiteiten die berichten naar elkaar verzenden. Elk object herkent een reeks berichten waarop het op zijn beurt antwoordt. Dit zijn openbaar methoden op een object. Privaat methoden zijn precies het tegenovergestelde. Ze zijn volledig intern voor een object en kunnen niet communiceren met iets buiten het object. Als openbare methoden verwant zijn aan berichten, lijken privémethoden op gedachten.

Het totaal van alle methoden, openbaar en privé, toegankelijk via openbare methoden, vertegenwoordigt het gedrag van een object. Bijvoorbeeld een voorwerp vertellen aan verhuizing oorzaken die niet alleen interageren met de interne methoden, maar ook met andere objecten. Vanuit het oogpunt van de gebruiker heeft het object slechts één eenvoudig gedrag: het moves.

Vanuit het oogpunt van de programmeur moet het object echter een heleboel kleine dingen doen om de beweging te bereiken.

Stel je bijvoorbeeld voor dat ons object een auto is. Om het te doen verhuizing, het moet een draaiende motor hebben, in de eerste versnelling (of omgekeerd) staan ​​en de wielen moeten draaien. Dit is een gedrag dat we moeten testen en waarop we moeten voortbouwen om onze productiecode te ontwerpen en te schrijven.


Stap 2: op afstand bestuurbare speelgoedauto

Onze geteste klasse gebruikt deze dummy-objecten nooit.

Laten we ons voorstellen dat we een programma bouwen om een ​​speelgoedauto op afstand te besturen. Alle commando's naar onze klas komen via de afstandsbediening. We moeten een klasse creëren die begrijpt wat de afstandsbediening verzendt en afgeeft commando's naar de auto.

Dit zal een oefenapplicatie zijn en we nemen aan dat de andere klassen die de verschillende delen van de auto besturen al zijn geschreven. We kennen de exacte handtekening van al deze klassen, maar helaas kon de autofabrikant ons geen prototype sturen - zelfs de broncode niet. Alles wat we weten, zijn de namen van de klassen, de methoden die ze hebben en welk gedrag elke methode omvat. De retourwaarden zijn ook opgegeven.


Stap 3: Applicatieschema

Hier is het volledige schema van de applicatie. Er is geen uitleg op dit punt; houd het eenvoudig in gedachten voor later gebruik.


Stap 4: Test dubbels

Een teststomp is een object om de indirecte invoer van de geteste code te besturen.

Mocking is een teststijl die een eigen set gereedschappen vereist, een reeks speciale objecten die verschillende niveaus van vervalsing van het gedrag van objecten vertegenwoordigen. Dit zijn:

  • dummy objecten
  • test stubs
  • test spionnen
  • test moppen
  • test vervalsingen

Elk van deze objecten heeft zijn speciale bereik en gedrag. In PHPUnit worden ze gemaakt met de $ This-> getMock () methode. Het verschil is hoe en voor welke redenen de objecten worden gebruikt.

Om deze objecten beter te begrijpen, zal ik de "Toy Car Controller" stap voor stap implementeren met behulp van de soorten objecten, in volgorde, zoals hierboven vermeld. Elk object in de lijst is complexer dan het object ervoor. Dit leidt tot een implementatie die radicaal anders is dan in de echte wereld. Omdat ik een imaginaire applicatie ben, zal ik een aantal scenario's gebruiken die misschien zelfs niet haalbaar zijn in een echte speelgoedauto. Maar goed, laten we ons voorstellen wat we nodig hebben om het grotere geheel te begrijpen.


Stap 5: Dummy-object

Dummy-objecten zijn objecten waarvan het System Under Test (SUT) afhankelijk is, maar ze worden nooit gebruikt. Een dummy-object kan een argument zijn dat aan een ander object wordt doorgegeven, of het kan worden geretourneerd door een tweede object en vervolgens worden doorgegeven aan een derde object. Het punt is dat onze geteste klasse deze dummy-objecten nooit echt gebruikt. Tegelijkertijd moet het object op een echt object lijken; anders kan de ontvanger het weigeren.

De beste manier om dit te illustreren is door een scenario voor te stellen; het schema waarvan, is hieronder:

Het oranje object is het RemoteControlTranslator. Het belangrijkste doel is om signalen van de afstandsbediening te ontvangen en deze te vertalen naar berichten voor onze klassen. Op een gegeven moment doet de gebruiker het "Klaar om te gaan" actie op de afstandsbediening. De vertaler ontvangt het bericht en maakt de klassen die nodig zijn om de auto klaar te maken.

De fabrikant zei dat "Klaar om te gaan" betekent dat de motor is gestart, dat de versnellingsbak in neutraal staat en dat de verlichting is ingeschakeld of uitgeschakeld op verzoek van de gebruiker.

Dit betekent dat de gebruiker de status van de lampjes vooraf kan bepalen voordat hij klaar is om te gaan, en dat deze worden in- of uitgeschakeld op basis van deze vooraf gedefinieerde waarde bij activering. RemoteControlTranslator stuurt dan alle nodige informatie naar de CarControl klasse' getReadyToGo ($ engine, $ gearbox, $ electronics, $ lights) methode. Ik weet dat dit allesbehalve een perfect ontwerp is en in strijd is met een paar principes en patronen, maar het is erg goed voor dit voorbeeld.

Start ons project met deze initiële bestandsstructuur:

Onthoud, alle klassen in de CarInterface map wordt geleverd door de fabrikant van de auto; we kennen de implementatie niet. Alles wat we weten zijn de handtekeningen van de klas, maar op dit moment geven we niet om hen.

Ons hoofddoel is om het CarController klasse. Om deze klasse te testen, moeten we ons voorstellen hoe we het willen gebruiken. Met andere woorden, we stellen onszelf in de schoenen van de RemoteControlTranslator en / of enige andere toekomstige klasse die mogelijk wordt gebruikt CarController. Laten we beginnen met het creëren van een case voor onze klas.

class CarControllerTest breidt PHPUnit_Framework_TestCase  uit

Voeg vervolgens een testmethode toe.

 function testItCanGetReadyTheCar () 

Denk nu na over wat we moeten doorgeven aan de getReadyToGo () methode: een motor, een versnellingsbak, een elektronica-controller en lichtinformatie. Omwille van dit voorbeeld, zullen we alleen de lichten bespotten:

require_once '... /CarController.php'; include '... /autoloadCarInterfaces.php'; klasse CarControllerTest breidt PHPUnit_Framework_TestCase uit function testItCanGetReadyTheCar () $ carController = new CarController (); $ engine = new Engine (); $ versnellingsbak = nieuwe versnellingsbak (); $ electornics = new Electronics (); $ dummyLights = $ this-> getMock ('Lights'); $ this-> assertTrue ($ carController-> getReadyToGo ($ engine, $ gearbox, $ electornics, $ dummyLights)); 

Dit zal uiteraard falen met:

PHP Fatale fout: aanroep op ongedefinieerde methode CarController :: getReadyToGo ()

Ondanks de mislukking gaf deze test ons een startpunt voor onze CarController implementatie. Ik heb een bestand opgenomen, genaamd autoloadCarInterfaces.php, dat stond niet op de oorspronkelijke lijst. Ik besefte dat ik iets nodig had om de lessen te laden, en ik schreef een heel basale oplossing. We kunnen het altijd herschrijven als de echte klassen worden aangeboden, maar dat is een heel ander verhaal. Voor nu houden we vast aan de eenvoudige oplossing:

foreach (scandir (dirname (__ FILE__). '/ CarInterface') als $ bestandsnaam) $ path = dirname (__ FILE__). '/ CarInterface /'. $ Filename; if (is_file ($ path)) require_once $ path; 

Ik neem aan dat deze klassenlader voor iedereen duidelijk is; dus laten we de testcode bespreken.

Eerst maken we een instantie van CarController, de klas die we willen testen. Vervolgens maken we instances van alle andere klassen waar we om geven: motor, versnellingsbak en elektronica.

We maken vervolgens een dummy Lichten object door PHPUnit's te bellen getMock () methode en het doorgeven van de naam van de Lichten klasse. Hiermee wordt een instantie van Lichten, maar elke methode komt terug nul--een dummy-object. Dit dummy-object kan niets doen, maar het geeft onze code de interface die nodig is om mee te werken Licht voorwerpen.

Het is heel belangrijk om dat op te merken $ dummyLights is een Lichten object en elke gebruiker die een verwacht Licht object kan het dummy-object gebruiken zonder te weten dat het geen echt object is Lichten voorwerp.

Om verwarring te voorkomen, raad ik aan het type van een parameter te specificeren bij het definiëren van een functie. Dit dwingt de PHP runtime om de argumenten te controleren die aan een functie zijn doorgegeven. Zonder het gegevenstype op te geven, kunt u elk object aan elke parameter doorgeven, wat kan resulteren in het falen van uw code. Met dit in gedachten, laten we de Elektronica klasse:

require_once 'Lights.php'; class Electronics functie turnOn (Lights $ lights) 

Laten we een test implementeren:

class CarController function getReadyToGo (Engine $ engine, Gearbox $ versnellingsbak, Electronics $ electronics, Lights $ lights) $ engine-> start (); $ Gearbox-> shift ( 'N'); $ Elektronica-> ZetAan ($ lichten); geef waar terug; 

Zoals u kunt zien, de getReadyToGo () functie gebruikte de $ lichten object voor het enige doel van het verzenden naar de $ elektronica voorwerpen aanzetten() methode. Is dit de ideale oplossing voor een dergelijke situatie? Waarschijnlijk niet, maar je kunt duidelijk zien hoe een dummy voorwerp, zonder enige relatie tot de getReadyToGo () functie, wordt doorgegeven aan het ene object dat het echt nodig heeft.

Houd er rekening mee dat alle klassen in de CarInterface map bieden dummy-objecten bij initialisatie. Neem ook aan dat we voor deze oefening verwachten dat de fabrikant in de toekomst de echte klassen levert. We kunnen niet vertrouwen op hun huidige gebrek aan functionaliteit; dus, we moeten ervoor zorgen dat onze tests slagen.


Stap 6: "Stub" de status en ga verder

Een teststomp is een object om de indirecte invoer van de geteste code te besturen. Maar wat is indirecte input? Het is een informatiebron die niet direct kan worden gespecificeerd.

Het meest gebruikelijke voorbeeld van een teststrook is wanneer een object een ander object om informatie vraagt ​​en vervolgens iets met die gegevens doet.

Spionnen zijn per definitie meer capabele stubs.

De gegevens kunnen alleen worden verkregen door er een specifiek object voor te vragen, en in veel gevallen worden deze objecten gebruikt voor een specifiek doel binnen de geteste klasse. We willen niet "nieuw" zijn (nieuw SomeClass ()) een klas in een andere klas voor testdoeleinden. Daarom moeten we een instantie van een klasse injecteren die zich gedraagt ​​als SomeClass zonder een echte te injecteren SomeClass voorwerp.

Wat we willen is een stompklasse, die dan leidt afhankelijkheid injectie. Dependency injection (DI) is een techniek die een object injecteert in een ander object, waardoor het wordt gedwongen het geïnjecteerde object te gebruiken. DI is gebruikelijk in TDD en het is absoluut vereist in bijna elk project. Het biedt een eenvoudige manier om een ​​object te dwingen een door de test voorbereide klasse te gebruiken in plaats van een echte klasse die in de productieomgeving wordt gebruikt.

Laten we onze speelgoedauto vooruit laten gaan.

We willen een methode implementeren genaamd moveForward (). Met deze methode wordt eerst een query uitgevoerd StatusPanel object voor de brandstof- en motorstatus. Als de auto klaar is om te vertrekken, geeft de methode de elektronica opdracht om te versnellen.

Om beter te begrijpen hoe een stub werkt, zal ik eerst de code voor de statuscontrole en -versnelling schrijven:

 functie goForward (Electronics $ electronics) $ statusPanel = nieuw StatusPanel (); if ($ statusPanel-> engineIsRunning () && $ statusPanel-> thereIsEnoughFuel ()) $ electronics-> accelerate (); 

Deze code is vrij eenvoudig, maar we hebben geen echte motor of brandstof om onze te testen ga vooruit() implementatie. Onze code komt niet eens in de als verklaring omdat we geen a hebben StatusPanel klasse. Maar als we doorgaan met de test, ontstaat er een logische oplossing:

 function testItCanAccelerate () $ carController = new CarController (); $ electronics = new Electronics (); $ stubStatusPanel = $ this-> getMock ('StatusPanel'); $ StubStatusPanel-> verwacht ($ this-> elke ()) -> methode ( 'thereIsEnoughFuel') -> zal ($ this-> returnValue (TRUE)); $ StubStatusPanel-> verwacht ($ this-> elke ()) -> methode ( 'engineIsRunning') -> zal ($ this-> returnValue (TRUE)); $ carController-> goForward ($ electronics, $ stubStatusPanel); 

Regel voor regel uitleg:

Ik hou van recursie; het is altijd gemakkelijker om recursie te testen dan lussen.

  • maak een nieuw CarController
  • maak de afhankelijke Elektronica voorwerp
  • maak een mock voor de StatusPanel
  • verwachten te bellen thereIsEnoughFuel () nul of meer keren en terugkeren waar
  • verwachten te bellen engineIsRunning () nul of meer keren en terugkeren waar
  • telefoontje ga vooruit() met Elektronica en StubbedStatusPanel voorwerp

Dit is de test die we willen schrijven, maar deze zal niet werken met onze huidige implementatie van ga vooruit(). We moeten het aanpassen:

 functie goForward (Electronics $ electronics, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : nieuw StatusPanel (); if ($ statusPanel-> engineIsRunning () && $ statusPanel-> thereIsEnoughFuel ()) $ electronics-> accelerate (); 

Onze modificatie gebruikt afhankelijkheid injectie door een tweede optionele parameter van het type toe te voegen StatusPanel. We bepalen of deze parameter een waarde heeft en een nieuwe maken StatusPanel als $ statusPanel is niets. Dit zorgt ervoor dat een nieuw StatusPanel object wordt gemaakt tijdens de productie, terwijl we toch de methode kunnen testen.

Het is belangrijk om het type van de te specificeren $ statusPanel parameter. Dit zorgt ervoor dat alleen een StatusPanel object (of een object van een geërfde klasse) kan worden doorgegeven aan de methode. Maar zelfs met deze wijziging is onze test nog steeds niet voltooid.


Stap 7: Voltooi de test met een echte proefversie

We moeten spot met een testen Elektronica object om ervoor te zorgen dat onze methode vanaf stap 6 oproepen versnellen(). We kunnen de realiteit niet gebruiken Elektronica klasse om verschillende redenen:

  • We hebben de klas niet.
  • We kunnen zijn gedrag niet verifiëren.
  • Zelfs als we het zouden kunnen noemen, zouden we het op zichzelf moeten testen.

Een testspot is een object dat zowel indirecte invoer als uitvoer kan controleren en heeft een mechanisme voor automatische bewering van verwachtingen en resultaten. Deze definitie klinkt misschien een beetje verwarrend, maar het is echt vrij eenvoudig om te implementeren:

 function testItCanAccelerate () $ carController = new CarController (); $ electronics = $ this-> getMock ('Electronics'); $ Elektronica-> verwacht ($ this-> eenmaal ()) -> methode ( 'versnellen'); $ stubStatusPanel = $ this-> getMock ('StatusPanel'); $ StubStatusPanel-> verwacht ($ this-> elke ()) -> methode ( 'thereIsEnoughFuel') -> zal ($ this-> returnValue (TRUE)); $ StubStatusPanel-> verwacht ($ this-> elke ()) -> methode ( 'engineIsRunning') -> zal ($ this-> returnValue (TRUE)); $ carController-> goForward ($ electronics, $ stubStatusPanel); 

We hebben gewoon de $ elektronica variabel. In plaats van een real te maken Elektronica object, we spotten er gewoon een.

Op de volgende regel definiëren we een verwachting op de $ elektronica voorwerp. Om precies te zijn, verwachten we dat het versnellen() methode wordt slechts één keer aangeroepen ($ This-> eenmaal ()). De test gaat nu voorbij!

Voel je vrij om met deze test te spelen. Probeer het te veranderen $ This-> eenmaal () in $ This-> precies (2) en zie wat een leuke foutmelding PHPUnit je geeft:

1) CarControllerTest :: testItCanAccelerate Verwachting mislukt voor methode naam is gelijk aan ; indien aangeroepen 2 tijd (s). Methode zou naar verwachting 2 keer worden genoemd, eigenlijk 1 keer genoemd.

Stap 8: Gebruik een testspion

Een testspion is een object dat indirecte uitvoer kan vastleggen en waar nodig indirecte invoer kan bieden.

Indirecte output is iets dat we niet rechtstreeks kunnen waarnemen. Bijvoorbeeld: wanneer de geteste klasse een waarde berekent en deze vervolgens gebruikt als argument voor de methode van een ander object. De enige manier om deze uitvoer te observeren, is door het aangeroepen object te vragen naar de variabele die wordt gebruikt om toegang te krijgen tot de methode.

Deze definitie maakt een spion bijna een mop.

Het belangrijkste verschil tussen een mock en een spion is dat mock-objecten ingebouwde beweringen en verwachtingen hebben.

In dat geval, hoe kunnen we een testspion maken met behulp van PHPUnit's getMock ()? We kunnen niet (nou ja, we kunnen geen pure spion maken), maar we kunnen moppen creëren die andere objecten kunnen bespioneren.

Laten we het remsysteem implementeren, zodat we de auto kunnen stoppen. Remmen is heel eenvoudig; de afstandsbediening detecteert de remintensiteit van de gebruiker en stuurt deze naar de controller. De afstandsbediening biedt ook een "noodstop!" knop. Dit moet onmiddellijk remmen inschakelen met maximaal vermogen.

Het remvermogen meet waarden van 0 tot 100, waarbij 0 niets betekent en 100 betekent maximale remkracht. De "Noodstop!" commando zal worden ontvangen als een andere oproep.

De CarController zal een bericht sturen naar de Elektronica object om het remsysteem te activeren. De autocontroller kan ook de StatusPanel voor snelheidsinformatie verkregen via sensoren op de auto.

Implementatie met een Pure Test Spy

Laten we eerst een puur spion-object implementeren zonder de mocking-infrastructuur van PHPUnit te gebruiken. Dit geeft u een beter begrip van het concept van de testspion. We beginnen met het controleren van de Elektronica de handtekening van het object.

klasse Elektronica functie turnOn (brandt $ lichten)  functie versnellen ()  functie pushBrakes ($ brakingPower) 

We zijn geïnteresseerd in de pushBrakes () methode. Ik heb het niet genoemd rem() om verwarring met de breken sleutelwoord in PHP.

Om een ​​echte spion te maken, zullen we uitbreiden Elektronica en negeer de pushBrakes () methode. Deze overbrugde methode drijft de rem niet; in plaats daarvan registreert het alleen de remkracht.

class SpyingElectronics breidt elektronica uit private $ brakingPower; functie pushBrakes ($ brakingPower) $ this-> brakingPower = $ brakingPower;  functie getBrakingPower () return $ this-> remkracht; 

De de getBrakingPower () methode geeft ons de mogelijkheid om de remkracht in onze test te controleren. Dit is geen methode die we zouden gebruiken in de productie.

We kunnen nu een test schrijven die de remkracht kan testen. Volgens de TDD-principes beginnen we met de eenvoudigste test en bieden we de meest eenvoudige implementatie:

 function testItCanStop () $ halfBrakingPower = 50; $ electronicsSpy = nieuwe SpyingElectronics (); $ carController = nieuwe CarController (); $ carController-> pushBrakes ($ halfBrakingPower, $ electronicsSpy); $ this-> assertEquals ($ halfBrakingPower, $ electronicsSpy-> getBrakingPower ()); 

Deze test mislukt omdat we nog geen hebben pushBrakes () methode op CarController. Laten we dat rechtzetten en er een schrijven:

 functie pushBrakes ($ brakingPower, Electronics $ electronics) $ electronics-> pushBrakes ($ brakingPower); 

De test is nu geslaagd en test de pushBrakes () methode.

We kunnen ook methodeaanroepen bespioneren. Het testen van de StatusPanel klasse is de volgende logische stap. Het biedt de gebruiker verschillende informatie over de op afstand bestuurbare auto. Laten we een test schrijven die controleert of het StatusPanel object wordt gevraagd over de snelheid van de auto. We zullen er een spion voor maken:

class SpyingStatusPanel breidt StatusPanel uit private $ speedWasRequested = false; function getSpeed ​​() $ this-> speedWasRequested = true;  function speedWhenRequested () return $ this-> speedWasRequested; 

Vervolgens passen we onze test aan om de spion te gebruiken:

 function testItCanStop () $ halfBrakingPower = 50; $ electronicsSpy = nieuwe SpyingElectronics (); $ statusPanelSpy = nieuw SpyingStatusPanel (); $ carController = nieuwe CarController (); $ carController-> pushBrakes ($ halfBrakingPower, $ electronicsSpy, $ statusPanelSpy); $ this-> assertEquals ($ halfBrakingPower, $ electronicsSpy-> getBrakingPower ()); $ This-> assertTrue ($ statusPanelSpy-> speedWasRequested ()); 

Merk op dat ik geen afzonderlijke test heb geschreven.

De aanbeveling van "één bevestiging per test" is goed om te volgen, maar wanneer uw test een actie beschrijft die meerdere stappen of toestanden vereist, is het gebruik van meer dan één bewering in dezelfde test aanvaardbaar.

Sterker nog, dit houdt uw beweringen over één enkel concept op één plek. Dit helpt om dubbele code te verwijderen door niet te vereisen dat u herhaaldelijk dezelfde voorwaarden voor uw SUT instelt.

En nu de implementatie:

 functie pushBrakes ($ brakingPower, Electronics $ electronics, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : nieuw StatusPanel (); $ Elektronica-> pushBrakes ($ brakingPower); $ StatusPanel-> getSpeed ​​(); 

Er is maar een klein, klein ding dat me dwarszit: de naam van deze test is testItCanStop (). Dat houdt duidelijk in dat we de remmen indrukken totdat de auto volledig tot stilstand is gekomen. We noemden echter de methode pushBrakes (), wat niet helemaal correct is. Tijd voor refactor:

 functie stop ($ brakingPower, Electronics $ electronics, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : nieuw StatusPanel (); $ Elektronica-> pushBrakes ($ brakingPower); $ StatusPanel-> getSpeed ​​(); 

Vergeet niet om ook de methodeaanroep in de test te veranderen.

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

Indirecte output is iets dat we niet rechtstreeks kunnen waarnemen.

Op dit punt moeten we nadenken over ons remsysteem en hoe het werkt. Er zijn verschillende mogelijkheden, maar neem voor dit voorbeeld aan dat de aanbieder van de speelgoedauto heeft aangegeven dat remmen in discrete intervallen plaatsvindt. Bellen Elektronica voorwerpen pushBreakes () methode duwt de rem gedurende een discrete tijd en laat hem vervolgens los. Het tijdsinterval is voor ons onbelangrijk, maar laten we ons voorstellen dat het een fractie van een seconde is. Met zo'n klein tijdsinterval moeten we continu verzenden pushBrakes () commando's totdat de snelheid nul is.

Spionnen zijn per definitie meer capabele stubs en ze kunnen ook indirecte invoer regelen indien nodig. Laten we onze maken StatusPanel spionnen beter in staat en bieden enige waarde voor de snelheid. Ik denk dat de eerste oproep een positieve snelheid moet bieden, laten we zeggen de waarde van 1. De tweede oproep geeft de snelheid van 0.

class SpyingStatusPanel breidt StatusPanel uit private $ speedWasRequested = false; privé $ currentSpeed ​​= 1; functie getSpeed ​​() if ($ this-> speedWasRequested) $ this-> currentSpeed ​​= 0; $ this-> speedWasRequested = true; return $ this-> currentSpeed;  function speedWhenRequested () return $ this-> speedWasRequested;  function spyOnSpeed ​​() return $ this-> currentSpeed; 

Het overschreven getSpeed ​​() methode retourneert de juiste snelheidswaarde via de spyOnSpeed ​​() methode. Laten we een derde bewering toevoegen aan onze test:

 function testItCanStop () $ halfBrakingPower = 50; $ electronicsSpy = nieuwe SpyingElectronics (); $ statusPanelSpy = nieuw SpyingStatusPanel (); $ carController = nieuwe CarController (); $ carController-> stop ($ halfBrakingPower, $ electronicsSpy, $ statusPanelSpy); $ this-> assertEquals ($ halfBrakingPower, $ electronicsSpy-> getBrakingPower ()); $ This-> assertTrue ($ statusPanelSpy-> speedWasRequested ()); $ this-> assertEquals (0, $ statusPanelSpy-> spyOnSpeed ​​()); 

Volgens de laatste bewering moet de snelheid een snelheidswaarde hebben van 0 na de hou op() methode voltooit uitvoering. Het uitvoeren van deze test tegen onze productiecode resulteert in een fout met een cryptisch bericht:

1) CarControllerTest :: testItCanStop Mislukt bewerend dat 1 overeenkomt met verwachte 0.

Laten we ons eigen beweringbericht toevoegen:

$ this-> assertEquals (0, $ statusPanelSpy-> spyOnSpeed ​​(), 'Verwachte snelheid is 0 (nul) na het stoppen, maar het was eigenlijk'. $ statusPanelSpy-> spyOnSpeed ​​());

Dat levert een veel leesbaarder foutbericht op:

1) CarControllerTest :: testItCanStop Verwachte snelheid is 0 (nul) na het stoppen maar het was in werkelijkheid 1 Mislukt bewerend dat 1 overeenkomt met verwachte 0.

Genoeg mislukkingen! Laten we het laten passeren.

 functie stop ($ brakingPower, Electronics $ electronics, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : nieuw StatusPanel (); $ Elektronica-> pushBrakes ($ brakingPower); if ($ statusPanel-> getSpeed ​​()) $ this-> stop ($ brakingPower, $ electronics, $ statusPanel); 

Ik hou van recursie; het is altijd gemakkelijker om recursie te testen dan lussen. Eenvoudiger testen betekent eenvoudiger code, wat op zijn beurt een beter algoritme betekent. Bekijk het The Transformation Priority Premise voor meer informatie over dit onderwerp.

Terug naar PHPUnit's Mocking Framework

Genoeg met de extra lessen. Laten we dit herschrijven met het spotkader van PHPUnit en die pure spionnen elimineren. Waarom?

Omdat PHPUnit een betere en eenvoudigere spot-syntaxis, minder code en een aantal leuke voorgedefinieerde methoden biedt.

Ik maak meestal alleen pure spionnen en stubs als ik ze bespreek getMock () zou te gecompliceerd zijn. Als je klassen zo complex zijn dat getMock () kan ze niet aan, dan heb je een probleem met je productiecode - niet met je tests.

 function testItCanStop () $ halfBrakingPower = 50; $ electronicsSpy = $ this-> getMock ('Electronics'); $ ElectronicsSpy-> verwacht ($ this-> precies (2)) -> methode ( 'pushBrakes') -> met ($ halfBrakingPower); $ statusPanelSpy = $ this-> getMock ('StatusPanel'); $ StatusPanelSpy-> verwacht ($ this-> aan (0)) -> methode ( 'getSpeed') -> zal ($ this-> returnValue (1)); $ StatusPanelSpy-> verwacht ($ this-> aan (1)) -> methode ( 'getSpeed') -> zal ($ this-> returnValue (0)); $ carController = nieuwe CarController (); $ carController-> stop ($ halfBrakingPower, $ electronicsSpy, $ statusPanelSpy); 

Het totaal van alle methoden, openbaar en privé, toegankelijk via openbare methoden, vertegenwoordigt het gedrag van een object.

Een regel voor regel uitleg van de bovenstaande code:

  • stel de halve remkracht in op 50
  • Creëer een Elektronica bespotten
  • verwacht methode pushBrakes () om precies twee keer uit te voeren met de hierboven gespecificeerde remkracht
  • Maak een StatusPanel bespotten
  • terugkeer 1 eerst getSpeed ​​() telefoontje
  • terugkeer 0 op de tweede plaats getSpeed ​​() uitvoering
  • bel de geteste hou op() methode op een echte CarController voorwerp

Waarschijnlijk het meest interessante in deze code is de $ This-> aan ($ someValue) methode. PHPUnit telt het aantal oproepen naar die spot. Tellen gebeurt op het nepniveau; dus meerdere methoden aanroepen $ statusPanelSpy zou de teller ophogen. Dit lijkt aanvankelijk een beetje contra-intuïtief; dus laten we een voorbeeld bekijken.

Stel dat we het brandstofniveau willen controleren bij elke oproep naar hou op(). De code zou er als volgt uitzien:

 functie stop ($ brakingPower, Electronics $ electronics, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : nieuw StatusPanel (); $ Elektronica-> pushBrakes ($ brakingPower); $ StatusPanel-> thereIsEnoughFuel (); if ($ statusPanel-> getSpeed ​​()) $ this-> stop ($ brakingPower, $ electronics, $ statusPanel); 

Dit zal onze test doorbreken. U bent misschien in de war waarom, maar u krijgt het volgende bericht:

1) CarControllerTest :: testItCanStop Verwachting mislukt voor methode naam is gelijk aan  indien aangeroepen 2 tijd (s). Methode zou naar verwachting 2 keer worden genoemd, eigenlijk 1 keer genoemd.

Het is vrij duidelijk dat pushBrakes () zou twee keer moeten worden aangeroepen. Waarom krijgen we dan deze boodschap? Vanwege de $ This-> aan ($ someValue) verwachting. De teller wordt als volgt verhoogd:

  • eerste oproep aan hou op() -> eerste oproep naar thereIsEnougFuel () => interne teller op 0
  • eerste oproep aan hou op() -> eerste oproep naar getSpeed ​​() => interne teller op 1 en terugkeer 0
  • tweede oproep aan hou op() gebeurt nooit => tweede oproep aan getSpeed ​​() gebeurt nooit

Elke oproep aan ieder bespotte methode $ statusPanelSpy verhoogt PHPUnit's interne teller.


Stap 9: Een test nep

Als openbare methoden verwant zijn aan berichten, lijken privémethoden op gedachten.

Een testnep is een eenvoudiger