Hvis du sammenligner PhpSpec med andre testrammer, vil du oppdage at det er et svært sofistikert og oppfunnet verktøy. En av grunnene til dette er at PhpSpec ikke er et testramme som de du allerede kjenner.
I stedet er det et designverktøy som bidrar til å beskrive atferd av programvare. En bivirkning av å beskrive oppførselen til programvare med PhpSpec, er at du vil ende opp med spesifikasjoner som også vil tjene som tester etterpå.
I denne artikkelen vil vi se under Hood of PhpSpec og forsøke å få en dypere forståelse av hvordan det fungerer og hvordan det skal brukes.
Hvis du ønsker å pusse opp på phpspec, ta en titt på min startveiledning.
La oss starte med å se på noen av hovedkonseptene og klassene som danner PhpSpec.
$ dette
Forstå hva $ dette
refererer til er nøkkelen til å forstå hvordan PhpSpec skiller seg fra andre verktøy. I utgangspunktet, $ dette
referer til en forekomst av den aktuelle klassen under testen. La oss prøve å undersøke dette litt mer for å bedre forstå hva vi mener.
Først og fremst trenger vi en spesiell og en klasse å leke med. Som du vet, gjør PhpSpecs generatorer dette super enkelt for oss:
$ phpspec desc "Suhm \ HelloWorld" $ phpspec løpe Vil du at jeg skal lage 'Suhm \ HelloWorld' for deg? y
Neste opp, åpne den genererte spesifikke filen, og la oss prøve å få litt mer informasjon om $ dette
:
shouldHaveType ( 'Suhm \ Hello'); var_dump (get_class ($ denne));
get_class ()
returnerer klassenavnet til en gitt gjenstand. I dette tilfellet kaster vi bare $ dette
der inne for å se hva den returnerer:
$ string (24) "spec \ Suhm \ HelloWorldSpec"
Ok, så ikke så overraskende, get_class ()
forteller oss det $ dette
er en forekomst av spec \ Suhm \ HelloWorldSpec
. Dette er fornuftig siden dette er tross alt bare vanlig gammel PHP-kode. Hvis i stedet brukte vi get_parent_class ()
, vi ville fåPhpSpec \ ObjectBehavior
, siden vår spesifikasjon utvider denne klassen.
Husk at jeg bare fortalte deg det $ dette
faktisk henvist til klassen under test, som ville væreSuhm \ Helloworld
i vårt tilfelle? Som du kan se, er returverdien for get_class ($ denne)
er i motsetning til $ Dette-> shouldHaveType ( 'Suhm \ Helloworld');
.
La oss prøve noe annet ut:
shouldHaveType ( 'Suhm \ Hello'); var_dump (get_class ($ denne)); $ Dette-> dumpThis () -> shouldReturn ( 'spec \ Suhm \ HelloWorldSpec');
Med den ovennevnte koden prøver vi å ringe en metode som heter dumpThis ()
på Hei Verden
forekomst. Vi kjedde en forventning til metodeanropet, og ventet at verdien av funksjonen skulle være en streng som inneholder"Spec \ Suhm \ HelloWorldSpec"
. Dette er returverdi fra get_class ()
på linjen over.
Igjen kan PhpSpec generatorer hjelpe oss med noen stillas:
$ phpspec run Vil du at jeg skal lage 'Suhm \ HelloWorld :: dumpThis ()' for deg? y
La oss prøve å ringe get_class ()
innenfra dumpThis ()
også:
Igjen, ikke overraskende, får vi:
10 ✘ det er initialiserbart forventet "spec \ Suhm \ HelloWorldSpec", men fikk "Suhm \ HelloWorld".Det ser ut til at vi mangler noe her. Jeg startet med å fortelle det
$ dette
refererer ikke til hva du tror det gjør, men så langt har våre eksperimenter ikke vist noe uventet. Unntatt en ting: Hvordan kunne vi ringe$ Dette-> dumpThis ()
før det var eksisterende uten PHP squeaking på oss?For å forstå dette må vi dykke inn i PhpSpec kildekoden. Hvis du vil ta en titt selv, kan du lese koden på GitHub.
Ta en titt på følgende kode fra
src / PhpSpec / ObjectBehavior.php
(den klassen som vår spesifikasjon utvider):/ ** * Proxyer alle ringe til PhpSpec-emnet * * @param string $ metode * @param array $ arguments * * @return mixed * / offentlig funksjon __call ($ metode, array $ arguments = array ()) return call_user_func_array array ($ this-> object, $ method), $ argumenter);Kommentarene gir det meste av det:
"Proxyer alle ringe til PhpSpec-emnet"
. PHP__anrop
Metode er en magisk metode som kalles automatisk når en metode ikke er tilgjengelig (eller ikke eksisterende).Dette betyr at når vi prøvde å ringe
$ Dette-> dumpThis ()
, anropet var tilsynelatende proxied til PhpSpec-emnet. Hvis du ser på koden, kan du se at metallsamtalen er proxied til$ Dette-> objekt
. (Det samme gjelder for egenskaper på vår forekomst. De er også proxied til emnet, med andre magiske metoder. Ta en titt i kilden for å se for deg selv.)La oss konsultere
get_class ()
en gang til og se hva den har å si om$ Dette-> objekt
:shouldHaveType ( 'Suhm \ Hello'); var_dump (get_class ($ dette-> objekt));Og se hva vi får:
streng (23) "PhpSpec \ Wrapper \ Subject"Mer på
Emne
Emne
er en wrapper og implementererPhpSpec \ Wrapper \ WrapperInterface
. Det er en sentral del av PhpSpec og gir mulighet for alle [tilsynelatende] magien som rammen kan gjøre. Det bryter om en forekomst av klassen vi tester, slik at vi kan gjøre alle slags ting som anropsmetoder og egenskaper som ikke eksisterer og sett forventninger.Som nevnt, er PhpSpec veldig opptatt av hvordan du skal skrive og spesifisere koden din. Én spesifikke kart til en klasse. Du har bare en emne per spesifikasjon, hvilken PhpSpec vil forsiktig pakke for deg. Det viktige å merke seg om dette er at dette lar deg bruke
$ dette
som om det var den faktiske forekomsten og gir virkelig lesbare og meningsfulle spesifikasjoner.PhpSpec inneholder a
wrapper
som tar seg av instantiating theEmne
. Den pakkerEmne
med det aktuelle objektet vi spesifiserer. SidenEmne
implementererWrapperInterface
det må ha agetWrappedObject ()
metode som gir oss tilgang til objektet. Dette er objektet forekomsten vi søkte etter tidligere medget_class ()
.La oss prøve det igjen:
shouldHaveType ( 'Suhm \ Hello'); var_dump (get_class ($ dette-> objekt> getWrappedObject ())); // Og bare for å være helt sikker: var_dump ($ this-> object-> getWrappedObject () -> dumpThis ());Og der går du:
$ leverandør / bin / phpspec løpestreng (15) "Suhm \ HelloWorld" streng (15) "Suhm \ HelloWorld"Selv om mange ting skjer bak scenen, til slutt jobber vi fortsatt med selve objektet forekomsten av
Suhm \ Helloworld
. Alt er bra.Tidligere da vi ringte
$ Dette-> dumpThis ()
, vi lærte hvordan anropet faktisk var proxied tilEmne
. Vi lærte også detEmne
er bare en wrapper og ikke selve objektet.Med denne kunnskapen er det klart at vi ikke kan ringe
dumpThis ()
påEmne
uten en annen magisk metode.Emne
har en__anrop()
metode også:/ ** * @param streng $ metode * @param array $ arguments * * @return mixed | Emne * / offentlig funksjon __call ($ metode, array $ arguments = array ()) hvis (0 === strpos , 'skal')) return $ this-> callExpectation ($ metode, $ argumenter); returnere $ this-> caller-> call ($ metode, $ argumenter);Denne metoden gjør en av to ting. Først sjekker det om metodenavnet begynner med 'skal'. Hvis det gjør det, er det en forventning, og anropet blir delegert til en metode som kalles
callExpectation ()
. Hvis ikke, blir anropet i stedet delegert til en forekomst avPhpSpec \ Wrapper \ Tema \ Caller
.Vi vil ignorere
Caller
for nå. Den inneholder også den innpakket gjenstanden og vet hvordan man skal ringe metoder på den. DeCaller
returnerer en innpakket forekomst når den kaller metoder om emnet, slik at vi kan kjede forventninger til metoder, som vi gjorde meddumpThis ()
.I stedet la oss ta en titt på
callExpectation ()
metode:/ ** * @param streng $ metode * @param array $ arguments * * @return mixed * / privat funksjon callExpectation ($ metode, array $ argumenter) $ subject = $ this-> makeSureWeHaveASubject (); $ expectation = $ this-> expectationFactory-> create ($ metode, $ emne, $ argumenter); hvis (0 === strpos ($ metode, 'shouldNot')) return $ expectation-> match (lcfirst (substr ($ metode, 9)), $ dette, $ argumenter, $ this-> wrappedObject); returnere $ forventning-> match (lcfirst (substr ($ metode, 6)), $ dette, $ argumenter, $ this-> wrappedObject);Denne metoden er ansvarlig for å bygge en forekomst av
PhpSpec \ Wrapper \ Tema \ Forventning \ ExpectationInterface
. Dette grensesnittet dikterer akamp()
metode, somcallExpectation ()
samtaler for å sjekke forventningen. Det er fire forskjellige typer forventninger:positiv
,Negativ
,PositiveThrow
ogNegativeThrow
. Hver av disse forventningene inneholder en forekomst avPhpSpec \ Matcher \ MatcherInterface
atkamp()
metoden bruker. La oss se på kampspillere neste.matchers
Matchere er det vi bruker til å bestemme oppførselen til våre objekter. Når vi skriver
bør…
ellerburde ikke…
, Vi bruker en matcher. Du finner en omfattende liste over PhpSpec-kampanjer på min personlige blogg.Det er mange matchere som følger med PhpSpec, som alle utvider
PhpSpec \ Matcher \ BasicMatcher
klassen, som implementererMatcherInterface
. Måten matchere arbeidet er ganske rett frem. La oss ta en titt på det sammen, og jeg oppfordrer deg til å ta en titt på kildekoden også.For eksempel, la oss se på denne koden fra
IdentityMatcher
:/ ** * @var array * / privat statisk $ keywords = array ('return', 'be', 'equal', 'beEqualTo'); / ** * @param streng $ navn * @param blandet $ emne * @param array $ arguments * * @return bool * / offentlig funksjon støtter ($ navn, $ emne, array $ argumenter) return in_array ($ navn, selv :: $ søkeord) && 1 == count ($ argumenter);De
støtter ()
Metoden er diktert avMatcherInterface
. I dette tilfellet fire aliaser er definert for matcheren i$ søkeord
array. Dette vil tillate at spilleren støtter enten:shouldReturn ()
,bør være()
,shouldEqual ()
ellershouldBeEqualTo ()
, ellershouldNotReturn ()
,burde ikke være()
,shouldNotEqual ()
ellershouldNotBeEqualTo ()
.Fra
BasicMatcher
, to metoder er arvet:positiveMatch ()
ognegativeMatch ()
. De ser slik ut:/ ** * @param streng $ navn * @param blandet $ emne * @param array $ arguments * * @return mixed * * @throws FailureException * / endelig offentlig funksjon positiveMatch ($ navn, $ emne, array $ argumenter) hvis (false === $ this-> matches ($ subject, $ arguments)) kaste $ this-> getFailureException ($ navn, $ emne, $ argumenter); returnere $ emne;De
positiveMatch ()
Metoden kaster et unntak hvisfyrstikker()
metode (abstrakt metode som kampførere må implementere) returnererfalsk
. DenegativeMatch ()
Metoden virker motsatt måte. Defyrstikker()
metode forIdentityMatcher
bruker===
operatør for å sammenligne$ emnet
med argumentet som følger med matcher-metoden:/ ** * @param blandet $ emne * @param array $ arguments * * @return bool * / beskyttet funksjonskomponenter ($ emne, array $ argumenter) return $ subject === $ arguments [0];Vi kunne bruke matcheren slik:
$ Dette-> getUser () -> shouldNotBeEqualTo ($ anotherUser);Som til slutt ville ringe
negativeMatch ()
og sørg for atfyrstikker()
returnerer false.Ta en titt på noen av de andre kampene og se hva de gjør!
Løfter om mer magi
Før vi avslutter denne korte rundturen til PhpSpec's internals, la oss se på en ekstra bit magi:
shouldHaveType ( 'Suhm \ Hello'); var_dump (get_class ($ objekt));Ved å legge til typen antydet
$ objekt
Parameter til vårt eksempel, PhpSpec vil automatisk bruke refleksjon for å injisere en forekomst av klassen for oss å bruke. Men med de tingene vi så allerede, stoler vi virkelig på at vi virkelig får en forekomst avStdClass
? La oss konsultereget_class ()
en gang til:$ leverandør / bin / phpspec løpestreng (28) "PhpSpec \ Wrapper \ Collaborator"Nei. I stedet for
StdClass
vi får en forekomst avPhpSpec \ Wrapper \ Samarbeidspartner
. Hva handler dette om?Som
Emne
,samarbeidspartner
er en wrapper og implementererWrapperInterface
. Det bryter en forekomst av\ Prophecy \ Prophecy \ ObjectProphecy
, som stammer fra profetien, det mocking-rammene som kommer sammen med PhpSpec. I stedet for enStdClass
eksempelvis, PhpSpec gir oss en mock. Dette gjør latterlig latterlig med PhpSpec, og lar oss legge til løfter til våre gjenstander som dette:$ Bruker-> getAge () -> willreturn (10); $ Dette-> setUser ($ bruker); $ Dette-> getUserStatus () -> shouldReturn ( 'barn');Med denne korte omvisningen av deler av PhpSpecs internals, håper jeg at du ser at det er mer enn et enkelt testramme.
Forskjellen mellom TDD og BDD
PhpSpec er et verktøy for å gjøre SpecBDD, så for å få bedre forståelse, la oss ta en titt på forskjellene mellom testdrevet utvikling (TDD) og atferdsdrevet utvikling (BDD). Etterpå tar vi en rask titt på hvordan PhpSpec skiller seg fra andre verktøy som PHPUnit.
TDD er konseptet om å la automatiserte tester drive design og implementering av kode. Ved å skrive små tester for hver funksjon, før vi faktisk implementerer dem, når vi får en beståttest, vet vi at vår kode tilfredsstiller den spesifikke funksjonen. Med en bestått test, etter refactoring, stopper vi koding og skriver neste test i stedet. Mantraet er "rødt", "grønt", "refaktor"!
BDD har sin opprinnelse fra - og er veldig lik - TDD. Ærlig, det er hovedsakelig et spørsmål om ordlyd, noe som virkelig er viktig siden det kan forandre måten vi tenker som utviklere. Når TDD snakker om testing, snakker BDD om å beskrive atferd.
Med TDD fokuserer vi på å verifisere at vår kode fungerer slik vi forventer at den skal fungere, mens vi med BDD fokuserer på å verifisere at vår kode faktisk oppfører seg som vi ønsker det. En hovedgrunn til fremveksten av BDD, som et alternativ til TDD, er å unngå å bruke ordet "test". Med BDD er vi ikke veldig interessert i å teste implementeringen av koden vår, vi er mer interessert i å teste hva det gjør (dets oppførsel). Når vi gjør BDD, i stedet for TDD, har vi historier og spesifikasjoner. Disse gjør at tradisjonelle tester blir overflødige.
Historier og spesifikasjoner er nært knyttet til forventningene til prosjektets interessenter. Skrivehistorier (med et verktøy som Behat), vil helst skje sammen med interessentene eller domeneeksperterne. Historiene dekker ekstern oppførsel. Vi bruker spesifikasjoner for å designe den interne oppføringen som trengs for å fullfylle trinnene i historiene. Hvert trinn i en historie kan kreve flere iterasjoner med skriving av spesifikasjoner og implementeringskode, før den er fornøyd. Våre historier, sammen med våre spesifikasjoner, hjelper oss å sørge for at vi ikke bare bygger en fungerende ting, men at det også er riktig. Som det har BDD mye å gjøre med kommunikasjon.
Hvordan er PhpSpec forskjellig fra PHPUnit?
For noen måneder siden skrev et bemerkelsesverdig medlem av PHP-samfunnet Mathias Verraes, "En enhetstesting rammeverk i en tweet" på Twitter. Poenget var å passe kildekoden til en funksjonell enhetstesting rammeverk i en enkelt tweet. Som du kan se fra kjernen, er koden virkelig funksjonell, og lar deg skrive grunnleggende enhetstester. Konseptet med enhetstesting er faktisk ganske enkelt: Sjekk noen form for påstand og varsle brukeren om resultatet.
Selvfølgelig er de fleste testrammer, som PHPUnit, faktisk langt mer avanserte, og kan gjøre mye mer enn Mathias rammebetingelser, men det viser fortsatt et viktig poeng: Du hevder noe, og så rammer du rammeverket for deg.
La oss ta en titt på en veldig grunnleggende PHPUnit-test:
offentlig funksjon testTrue () $ this-> assertTrue (false);Vil du kunne skrive en super enkel implementering av et testramme som kan kjøre denne testen? Jeg er ganske sikker på at svaret er "ja" du kan gjøre det. Tross alt er det eneste som
assertTrue ()
Metoden har å gjøre er å sammenligne en verdi motekte
og kaste unntak hvis det mislykkes. I kjernen, det som skjer er faktisk ganske rett frem.Så hvordan er PhpSpec forskjellig? Først av alt, er PhpSpec ikke et testverktøy. Testing av koden er ikke hovedmålet for PhpSpec, men det blir en bivirkning hvis du bruker den til å designe programvaren din ved å inkrementere legge til spesifikasjoner for oppførselen (BDD).
For det andre tror jeg at de ovennevnte avsnittene allerede har gjort det klart hvordan PhpSpec er forskjellig. Likevel, la oss sammenligne noen kode:
// PhpSpec funksjon it_is_initializable () $ this-> shouldHaveType ('Suhm \ HelloWorld'); // PHPUnit funksjon testIsInitializable () $ object = new Suhm \ HelloWorld (); $ this-> assertInstanceOf ('Suhm \ HelloWorld', $ objekt);Fordi PhpSpec er svært oppfylt og gjør noen påstander om hvordan vår kode er utformet, gir den oss en veldig enkel måte å beskrive vår kode på. På den annen side gjør PHPUnit ingen påstander mot vår kode og lar oss gjøre ganske mye hva vi vil. I utgangspunktet er alt PHPUnit for oss i dette eksempelet, å kjøre
$ objekt
mottilfelle av
operatør.Selv om PHPUnit kan virke lettere å komme i gang med (jeg tror ikke det er), hvis du ikke er forsiktig, kan du enkelt falle i feller av dårlig design og arkitektur fordi det lar deg gjøre nesten alt. Når det er sagt, kan PHPUnit fortsatt være bra for mange brukstilfeller, men det er ikke et designverktøy som PhpSpec. Det er ingen veiledning - du må vite hva du gjør.
PhpSpec: Et designverktøy
Fra PhpSpec nettsiden kan vi lære at PhpSpec er:
Et php verktøysett for å drive fremkallende design ved spesifikasjon.La meg si det enda en gang: PhpSpec er ikke et testramme. Det er et utviklingsverktøy. Et programvareverktøy for design. Det er ikke et enkelt påstandsramme som sammenligner verdier og kaster unntak. Det er et verktøy som hjelper oss med å designe og bygge godt utformet kode. Det krever at vi tenker på strukturen i koden vår og håndhever visse arkitektoniske mønstre, hvor en klasse kartlegger en spesifikasjon. Hvis du bryter prinsippet om enkeltansvar og trenger å mocke noe, vil du ikke få lov til å gjøre det.
Gledelig spesiell!
Åh! Og til slutt = siden PhpSpec selv er bestemt, foreslår jeg at du går til GitHub og undersøker kilden for å lære mer.