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.
Laten we beginnen met een aantal van de belangrijkste concepten en klassen te bekijken die PhpSpec vormen.
$ 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 hetPhpSpec \ 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 deOnderwerpen
. Het packt deOnderwerpen
met het werkelijke object dat we specificeren. SindsOnderwerpen
implementeert deWrapperInterface
het moet een hebbengetWrappedObject ()
methode die ons toegang geeft tot het object. Dit is de objectinstantie waar we eerder naar zochtenget_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 deOnderwerpen
. Dat hebben we ook geleerdOnderwerpen
is alleen een verpakking en niet het werkelijke object.Met deze kennis is het duidelijk dat we niet kunnen bellen
dumpThis ()
opOnderwerpen
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 vanPhpSpec \ Wrapper \ Subject \ Caller
.We zullen de
bezoeker
voor nu. Het bevat ook het ingepakte object en weet methoden aan te roepen. Debezoeker
retourneert een ingepakt exemplaar wanneer het methoden in het onderwerp aanroept, waardoor we de verwachtingen kunnen afstemmen op methoden, zoals we dat dedendumpThis ()
.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 awedstrijd()
methode, die decallExpectation ()
oproepen om de verwachting te controleren. Er zijn vier verschillende soorten verwachtingen:Positief
,Negatief
,PositiveThrow
enNegativeThrow
. Elk van deze verwachtingen bevat een instantie vanPhpSpec \ Matcher \ MatcherInterface
dat dewedstrijd()
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 ...
ofzou 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 hetMatcherInterface
. 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 deMatcherInterface
. 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 ()
, ofshouldNotReturn ()
,zou niet moeten zijn()
,shouldNotEqual ()
ofshouldNotBeEqualTo ()
.Van de
BasicMatcher
, twee methoden zijn geërfd:positiveMatch ()
ennegativeMatch ()
. 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 dewedstrijden()
methode (abstracte methode die matchers moeten implementeren) retourneertvals
. DenegativeMatch ()
methode werkt het tegenovergestelde. Dewedstrijden()
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 datwedstrijden()
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 krijgenstdClass
? Laten we het raadplegenget_class ()
nog een keer:$ vendor / bin / phpspec run string (28) "PhpSpec \ Wrapper \ Collaborator"Nee. In plaats van
stdClass
we krijgen een instantie vanPhpSpec \ Wrapper \ Collaborator
. Waar gaat dit over?Net zoals
Onderwerpen
,Medewerker
is een verpakking en implementeert hetWrapperInterface
. Het wikkelt een instantie van\ Profetie \ Prophecy \ ObjectProphecy
, die voortkomt uit Prophecy, het spotkader dat samenkomt met PhpSpec. In plaats van eenstdClass
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 metwaar
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.