PhpSpec begrijpen

Als u PhpSpec vergelijkt met andere testframeworks, zult u merken dat het een zeer geavanceerd en eigenwijs hulpmiddel is. Een van de redenen hiervoor is dat PhpSpec geen testraamwerk is zoals het raamwerk dat u al kent. 

In plaats daarvan is het een ontwerptool die het gedrag van software helpt beschrijven. Een neveneffect van het beschrijven van het gedrag van software met PhpSpec is dat je uiteindelijk met specificaties komt te staan ​​die later ook als test dienen.

In dit artikel zullen we een kijkje nemen onder de motorkap van PhpSpec en proberen een beter begrip te krijgen van hoe het werkt en hoe het te gebruiken.

Als je phpspec wilt opfrissen, bekijk dan mijn zelfstudie.

In dit artikel…

  • Een snelle rondleiding door PhpSpec Internals
  • Het verschil tussen TDD en BDD
  • Hoe is PhpSpec anders (van PHPUnit)
  • PhpSpec: een ontwerptool

Een snelle rondleiding door PhpSpec Internals

Laten we beginnen met een aantal van de belangrijkste concepten en klassen te bekijken die PhpSpec vormen.

Begrip $ this

Begrijpen wat $ this verwijst naar is essentieel om te begrijpen hoe PhpSpec verschilt van andere hulpmiddelen. Eigenlijk, $ this verwijs naar een instantie van de eigenlijke klasse die getest wordt. Laten we proberen dit een beetje meer te onderzoeken om beter te begrijpen wat we bedoelen.

Allereerst hebben we een spec en een klas nodig om mee te spelen. Zoals u weet, maakt de generators van PhpSpec dit super gemakkelijk voor ons:

$ phpspec desc "Suhm \ HelloWorld" $ phpspec run Wil je dat ik 'Suhm \ HelloWorld' voor je maak? Y 

Open vervolgens het gegenereerde spec-bestand en probeer wat meer informatie over te krijgen $ this:

shouldHaveType (Suhm \ HelloWorld "); var_dump (get_class ($ this));  

get_class () retourneert de klassenaam van een bepaald object. In dit geval gooien we gewoon $ this om te zien wat het oplevert:

$ string (24) "spec \ Suhm \ HelloWorldSpec"

Oké, dus niet zo verrassend, get_class () vertelt ons dat $ this is een instantie van spec \ Suhm \ HelloWorldSpec. Dit is logisch, want dit is tenslotte net duidelijke oude PHP-code. Als we in plaats daarvan hebben gebruikt get_parent_class (), we zouden krijgenPhpSpec \ ObjectBehavior, omdat onze specificatie deze klasse uitbreidt.

Vergeet niet dat ik je dat net heb verteld $ this eigenlijk verwezen naar de klasse die getest wordt, wat zou zijnSuhm \ HelloWorld in ons geval? Zoals je kunt zien is de geretourneerde waarde van get_class ($ this) is in tegenspraak met $ This-> shouldHaveType ( 'Suhm \ HelloWorld');.

Laten we iets anders proberen:

shouldHaveType (Suhm \ HelloWorld "); var_dump (get_class ($ this)); $ This-> dumpThis () -> shouldReturn ( 'spec \ Suhm \ HelloWorldSpec');  

Met de bovenstaande code proberen we een methode genoemd te noemen dumpThis () op de Hallo Wereld aanleg. We ketenen een verwachting naar de methodeaanroep, in de verwachting dat de retourwaarde van de functie een string is met"Spec \ Suhm \ HelloWorldSpec". Dit is de retourwaarde van get_class () op de regel hierboven.

Nogmaals, de PhpSpec-generatoren kunnen ons helpen met wat steigers:

$ phpspec run Wilt u dat ik 'Suhm \ HelloWorld :: dumpThis ()' voor u maak? Y 

Laten we proberen te bellen get_class () van binnenuit dumpThis () te:

Nogmaals, niet verrassend, we krijgen:

 10 ✘ het is te verwachten "spec \ Suhm \ HelloWorldSpec", maar heeft "Suhm \ HelloWorld". 

Het lijkt erop dat we hier iets missen. Ik begon met je dat te vertellen $ this verwijst niet naar wat je denkt dat het doet, maar tot nu toe hebben onze experimenten niets onverwachts laten zien. Behalve één ding: hoe kunnen we bellen $ This-> dumpThis () voordat het bestond zonder dat PHP naar ons piepte?

Om dit te begrijpen, moeten we een duik nemen in de PhpSpec-broncode. Als je zelf een kijkje wilt nemen, kun je de code lezen op GitHub.

Kijk eens naar de volgende code van src / PhpSpec / ObjectBehavior.php (de klasse die onze specificatie uitbreidt):

/ ** * Proxy's bellen allemaal naar het onderwerp van PhpSpec * * @param string $ methode * @param array $ arguments * * @return mixed * / public function __call ($ methode, array $ arguments = array ()) return call_user_func_array ( array ($ dit-> object, $ methode), $ argumenten);  

De opmerkingen geven het meeste weg: "Proxy's bellen allemaal naar het PhpSpec-onderwerp". De PHP __call methode is een magische methode die automatisch wordt aangeroepen wanneer een methode niet toegankelijk is (of niet bestaat). 

Dit betekent dat toen we probeerden te bellen $ This-> dumpThis (), de oproep was schijnbaar proxied aan het PhpSpec-onderwerp. Als u naar de code kijkt, kunt u zien dat de methodeaanroep naar de proxy wordt gestuurd $ This-> object. (Hetzelfde geldt voor eigenschappen op ons exemplaar, ze zijn ook allemaal met het onderwerp in verband gebracht, met behulp van andere magische methoden. Neem een ​​kijkje in de bron om het zelf te zien.)

Laten we het raadplegen get_class () nog een keer en zie wat het te zeggen heeft $ This-> object:

shouldHaveType (Suhm \ HelloWorld "); var_dump (get_class ($ this-> object));  

En kijk eens wat we krijgen:

string (23) "PhpSpec \ Wrapper \ Subject"

Meer Onderwerpen

Onderwerpen is een verpakking en implementeert het PhpSpec \ Wrapper \ WrapperInterface. Het is een kernonderdeel van PhpSpec en biedt alle [schijnbaar] magie die het raamwerk kan bieden. Het wikkelt een exemplaar van de klasse die we testen, zodat we allerlei dingen kunnen doen, zoals belmethoden en eigenschappen die niet bestaat en verwachtingen stellen. 

Zoals vermeld, is PhpSpec erg nieuwsgierig naar hoe u uw code moet schrijven en specificeren. Eén specificatie wijst naar één klasse. Je hebt alleen een onderwerp per specificatie, die PhpSpec zorgvuldig voor u zal inpakken. Het belangrijkste om op te merken is dat dit je in staat stelt om te gebruiken $ this alsof het de echte instantie was en zorgt voor echt leesbare en betekenisvolle specificaties.

PhpSpec bevat een Wikkel die zorgt voor het instantiëren van de Onderwerpen. Het packt de Onderwerpen met het werkelijke object dat we specificeren. Sinds Onderwerpen implementeert de WrapperInterface het moet een hebben getWrappedObject ()methode die ons toegang geeft tot het object. Dit is de objectinstantie waar we eerder naar zochten get_class ()

Laten we het opnieuw proberen:

shouldHaveType (Suhm \ HelloWorld "); var_dump (get_class ($ this-> object-> getWrappedObject ())); // En om helemaal zeker te zijn: var_dump ($ this-> object-> getWrappedObject () -> dumpThis ());  

En daar ga je:

$ vendor / bin / phpspec run string (15) "Suhm \ HelloWorld" string (15) "Suhm \ HelloWorld" 

Hoewel er veel dingen achter de schermen aan de hand zijn, werken we uiteindelijk nog steeds met de eigenlijke objectinstantie van Suhm \ HelloWorld. Alles goed.

Eerder, toen we belden $ This-> dumpThis (), we hebben geleerd hoe de oproep daadwerkelijk is toegewezen aan de Onderwerpen. Dat hebben we ook geleerd Onderwerpen is alleen een verpakking en niet het werkelijke object. 

Met deze kennis is het duidelijk dat we niet kunnen bellen dumpThis () op Onderwerpen zonder een andere magische methode. Onderwerpen heeft een __call () methode ook:

/ ** * @param string $ methode * @param array $ arguments * * @return mixed | Subject * / public function __call ($ methode, array $ arguments = array ()) if (0 === strpos ($ methode , 'should')) return $ this-> callExpectation ($ methode, $ argumenten);  return $ this-> caller-> call ($ methode, $ argumenten);  

Deze methode doet een van twee dingen. Eerst controleert het of de methode naam begint met 'zou moeten'. Als dat zo is, is het een verwachting en wordt de aanroep gedelegeerd naar een methode met de naam callExpectation (). Als dit niet het geval is, wordt de aanroep in plaats daarvan gedelegeerd aan een instantie van PhpSpec \ Wrapper \ Subject \ Caller

We zullen de bezoeker voor nu. Het bevat ook het ingepakte object en weet methoden aan te roepen. De bezoeker retourneert een ingepakt exemplaar wanneer het methoden in het onderwerp aanroept, waardoor we de verwachtingen kunnen afstemmen op methoden, zoals we dat deden dumpThis ().

Laten we in plaats daarvan een kijkje nemen naar de callExpectation () methode:

/ ** * @param string $ methode * @param array $ arguments * * @return mixed * / private functie callExpectation ($ methode, array $ argumenten) $ subject = $ this-> makeSureWeHaveASubject (); $ verwachting = $ dit-> expectationFactory-> create ($ methode, $ onderwerp, $ argumenten); if (0 === strpos ($ methode, 'shouldNot')) return $ verwachting-> match (lcfirst (substr ($ method, 9)), $ this, $ arguments, $ this-> wrappedObject);  return $ verwachting-> match (lcfirst (substr ($ method, 6)), $ this, $ arguments, $ this-> wrappedObject);  

Deze methode is verantwoordelijk voor het bouwen van een instantie van PhpSpec \ Wrapper \ onderwerp \ Verwachting \ ExpectationInterface. Deze interface dicteert a wedstrijd() methode, die de callExpectation () oproepen om de verwachting te controleren. Er zijn vier verschillende soorten verwachtingen: PositiefNegatiefPositiveThrow en NegativeThrow. Elk van deze verwachtingen bevat een instantie van PhpSpec \ Matcher \ MatcherInterface dat de wedstrijd() methode gebruikt. Laten we nu naar matchers kijken.

matchers

Matchers zijn wat we gebruiken om het gedrag van onze objecten te bepalen. Wanneer we schrijven moet ...  of zou niet… , we gebruiken een matcher. Je kunt een uitgebreide lijst van PhpSpec-matchers vinden op mijn persoonlijke blog.

Er zijn veel matchers meegeleverd met PhpSpec, die allemaal het PhpSpec \ Matcher \ BasicMatcher klasse, die het MatcherInterface. De manier waarop matchers werken is redelijk rechttoe rechtaan. Laten we er samen naar kijken en ik moedig je aan ook de broncode te bekijken.

Laten we als voorbeeld deze code bekijken vanuit de IdentityMatcher:

/ ** * @ var array * / private static $ keywords = array ('return', 'be', 'equal', 'beEqualTo'); / ** * @param string $ naam * @param mixed $ subject * @param array $ arguments * * @return bool * / public function supports ($ name, $ subject, array $ arguments) return in_array ($ name, self :: $ keywords) && 1 == count ($ arguments);  

De steunen () methode wordt gedicteerd door de MatcherInterface. In dit geval vier aliassen zijn gedefinieerd voor de matcher in de $ trefwoorden matrix. Hierdoor kan de matcher ondersteuning bieden voor: shouldReturn ()zou moeten zijn()shouldEqual () ofshouldBeEqualTo (), of shouldNotReturn ()zou niet moeten zijn()shouldNotEqual () of shouldNotBeEqualTo ().

Van de BasicMatcher, twee methoden zijn geërfd: positiveMatch () en negativeMatch (). Ze zien er als volgt uit:

/ ** * @param string $ naam * @param mixed $ subject * @param array $ arguments * * @return mixed * * @throws FailureException * / final public function positiveMatch ($ name, $ subject, array $ arguments) if (false === $ this-> matches ($ subject, $ arguments)) gooi $ this-> getFailureException ($ name, $ subject, $ arguments);  return $ subject;  

De positiveMatch () methode werpt een uitzondering als de wedstrijden() methode (abstracte methode die matchers moeten implementeren) retourneert vals. De negativeMatch () methode werkt het tegenovergestelde. De wedstrijden() methode voor deIdentityMatcher gebruikt de === operator om het te vergelijken $ subject met het argument dat aan de matchermethode wordt geleverd:

/ ** * @param mixed $ subject * @param array $ arguments * * @return bool * / protected function matches ($ subject, array $ arguments) return $ subject === $ arguments [0];  

We zouden de matcher als volgt kunnen gebruiken:

$ This-> getUser () -> shouldNotBeEqualTo ($ anotherUser); 

Die zou uiteindelijk bellen negativeMatch () en zorg ervoor dat wedstrijden() geeft false terug.

Bekijk enkele van de andere matchers en kijk wat ze doen!

Beloften van Meer Magie

Voordat we deze korte tour van PhpSpec's internals beëindigen, laten we nog een stukje magie bekijken:

shouldHaveType (Suhm \ HelloWorld "); var_dump (get_class ($ object));  

Door het hintte type toe te voegen $ object parameter voor ons voorbeeld, PhpSpec zal automatisch reflectie gebruiken om een ​​instantie van de klasse te injecteren die we kunnen gebruiken. Maar met de dingen die we al zagen, vertrouwen we er echt op dat we er echt een voorbeeld van krijgen stdClass? Laten we het raadplegen get_class () nog een keer:

$ vendor / bin / phpspec run string (28) "PhpSpec \ Wrapper \ Collaborator" 

Nee. In plaats van stdClass we krijgen een instantie van PhpSpec \ Wrapper \ Collaborator. Waar gaat dit over?

Net zoals OnderwerpenMedewerker is een verpakking en implementeert het WrapperInterface. Het wikkelt een instantie van\ Profetie \ Prophecy \ ObjectProphecy, die voortkomt uit Prophecy, het spotkader dat samenkomt met PhpSpec. In plaats van een stdClass Zo geeft PhpSpec ons bijvoorbeeld een schijnvertoning. Dit maakt het spotten lachwekkend gemakkelijk met PhpSpec en stelt ons in staat om beloftes toe te voegen aan onze objecten zoals deze:

$ Gebruiksvriendelijkheid> getAge () -> willReturn (10); $ This-> setUser ($ user); $ This-> getUserStatus () -> shouldReturn ( 'kind'); 

Met deze korte rondleiding door delen van PhpSpec's internals, hoop ik dat je ziet dat het meer is dan een eenvoudig testraamwerk.

Het verschil tussen TDD en BDD

PhpSpec is een hulpmiddel om SpecBDD te doen, dus om een ​​beter begrip te krijgen, laten we eens kijken naar de verschillen tussen testgestuurde ontwikkeling (TDD) en gedragsgestuurde ontwikkeling (BDD). Daarna zullen we snel bekijken hoe PhpSpec verschilt van andere tools zoals PHPUnit.

TDD is het concept om geautomatiseerde tests het ontwerp en de implementatie van code mogelijk te maken. Door het schrijven van kleine tests voor elke functie, voordat deze daadwerkelijk worden geïmplementeerd, weten we dat onze code voldoet aan die specifieke functie wanneer we een slaagtest krijgen. Met een passerende test stoppen we na het refactoring met het coderen en schrijven we de volgende test. De mantra is "rood", "groen", "refactor"!

BDD komt voort uit - en lijkt veel op - TDD. Eerlijk gezegd is het vooral een kwestie van formulering, wat inderdaad belangrijk is, omdat het de manier waarop wij als ontwikkelaars denken kan veranderen. Waar TDD praat over testen, praat BDD over het beschrijven van gedrag. 

Met TDD richten we ons op het verifiëren dat onze code werkt zoals we verwachten dat deze werkt, terwijl we bij BDD zich richten op het verifiëren dat onze code zich daadwerkelijk gedraagt ​​zoals we dat willen. Een belangrijke reden voor het ontstaan ​​van BDD, als alternatief voor TDD, is om het gebruik van het woord "test" te vermijden. Met BDD zijn we niet echt geïnteresseerd in het testen van de implementatie van onze code, we zijn meer geïnteresseerd in het testen van wat het doet (zijn gedrag). Wanneer we BDD doen, in plaats van TDD, hebben we verhalen en specificaties. Dit maakt het schrijven van traditionele tests overbodig.

Verhalen en specificaties zijn nauw verbonden met de verwachtingen van de belanghebbenden van het project. Het schrijven van verhalen (met een tool zoals Behat) gebeurt bij voorkeur samen met de stakeholders of domeinexperts. De verhalen behandelen het externe gedrag. We gebruiken specificaties om het interne gedrag te ontwerpen dat nodig is om de stappen van de verhalen te voltooien. Elke stap in een verhaal kan meerdere iteraties vereisen met schrijfspecificaties en implementatiecode, voordat deze tevreden is. Onze verhalen, samen met onze specificaties, helpen ons ervoor te zorgen dat we niet alleen een werkend ding opbouwen, maar dat het ook het juiste is. BDD heeft dus veel te maken met communicatie.

Hoe is PhpSpec anders van PHPUnit?

Een paar maanden geleden plaatste een opmerkelijk lid van de PHP-community, Mathias Verraes, "Een eenheidstestkader in een tweet" op Twitter. Het punt was om de broncode van een functioneel unit testing-raamwerk in één enkele tweet te passen. Zoals je kunt zien aan de hand van de essentie, is de code echt functioneel en kun je basiseenheidstests schrijven. Het concept van eenheidstesten is eigenlijk vrij eenvoudig: controleer een soort van bewering en stel de gebruiker op de hoogte van het resultaat.

Natuurlijk zijn de meeste testkaders, zoals PHPUnit, inderdaad veel geavanceerder en kunnen ze veel meer doen dan het kader van Mathias, maar het toont nog steeds een belangrijk punt: je beweert iets en dan voert je framework die bewering voor jou uit.

Laten we een heel eenvoudige PHPUnit-test bekijken:

openbare functie testTrue () $ this-> assertTrue (false);  

Zou je in staat zijn om een ​​supereenvoudige implementatie van een testraamwerk te schrijven dat deze test zou kunnen uitvoeren? Ik ben er vrij zeker van dat het antwoord "ja" is, dat je dat zou kunnen doen. Immers, het enige wat het assertTrue () methode moet doen is een waarde vergelijken met waar en werp een uitzondering als het faalt. In de kern is wat er aan de hand is eigenlijk vrij eenvoudig.

Dus hoe is PhpSpec anders? Allereerst is PhpSpec geen testinstrument. Het testen van uw code is niet het hoofddoel van PhpSpec, maar het wordt een bijwerking als u het gebruikt om uw software te ontwerpen door stapsgewijs specs toe te voegen voor het gedrag (BDD). 

Ten tweede denk ik dat de bovenstaande paragrafen al duidelijk hadden moeten maken hoe PhpSpec anders is. Laten we toch een aantal codes vergelijken:

// PhpSpec function it_is_initializable () $ this-> shouldHaveType ('Suhm \ HelloWorld');  // PHPUnit function testIsInitializable () $ object = new Suhm \ HelloWorld (); $ this-> assertInstanceOf ('Suhm \ HelloWorld', $ object);  

Omdat PhpSpec zeer eigenzinnig is en een aantal beweringen doet over hoe onze code is ontworpen, geeft dit ons een zeer eenvoudige manier om onze code te beschrijven. Aan de andere kant maakt PHPUnit geen enkele bewering met betrekking tot onze code en laat ons vrijwel alles doen wat we willen. In principe moet alle PHPUnit voor ons in dit voorbeeld worden uitgevoerd $ object tegen deinstanceof operator. 

Hoewel PHPUnit gemakkelijker lijkt om ermee aan de slag te gaan (ik denk het niet), kun je gemakkelijk vallen in valstrikken van slecht ontwerp en architectuur omdat je bijna alles kunt doen. Dat gezegd hebbende, PHPUnit kan nog steeds geweldig zijn voor veel use-cases, maar het is geen ontwerptool zoals PhpSpec. Er is geen begeleiding - je moet weten wat je doet.

PhpSpec: een ontwerptool

Van de PhpSpec-website kunnen we leren dat PhpSpec is:

Een php-toolset om emergent design op specificatie te sturen.

Laat ik het nog een keer zeggen: PhpSpec is geen testkader. Het is een ontwikkelingshulpmiddel. Een software-ontwerpprogramma. Het is geen eenvoudig assertiekader dat waarden vergelijkt en uitzonderingen gooit. Het is een hulpmiddel dat ons helpt bij het ontwerpen en bouwen van goed gemaakte code. Het vereist dat we nadenken over de structuur van onze code en bepaalde architecturale patronen afdwingen, waarbij één klasse naar één specificatie toewijst. Als je het principe van de enkele verantwoordelijkheid doorbreekt en iets gedeeltelijk moet bespotten, mag je het niet doen.

Veel plezier!

Oh! En tot slot, omdat PhpSpec zelf gespecificeerd is, stel ik voor dat je naar GitHub gaat en de bron verkent om meer te weten te komen.