Forstå PhpSpec

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.

I denne artikkelen…

  • En rask tur til PhpSpec Internals
  • Forskjellen mellom TDD og BDD
  • Hvordan er PhpSpec forskjellig (fra PHPUnit)
  • PhpSpec: Et designverktøy

En rask tur til PhpSpec Internals

La oss starte med å se på noen av hovedkonseptene og klassene som danner PhpSpec.

forståelse $ 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 implementerer PhpSpec \ 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 the Emne. Den pakker Emne med det aktuelle objektet vi spesifiserer. Siden Emne implementerer WrapperInterface det må ha a getWrappedObject ()metode som gir oss tilgang til objektet. Dette er objektet forekomsten vi søkte etter tidligere med get_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 til Emne. Vi lærte også det Emne 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 av PhpSpec \ Wrapper \ Tema \ Caller

Vi vil ignorere Caller for nå. Den inneholder også den innpakket gjenstanden og vet hvordan man skal ringe metoder på den. De Caller returnerer en innpakket forekomst når den kaller metoder om emnet, slik at vi kan kjede forventninger til metoder, som vi gjorde med dumpThis ().

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 a kamp() metode, som callExpectation () samtaler for å sjekke forventningen. Det er fire forskjellige typer forventninger: positivNegativPositiveThrow og NegativeThrow. Hver av disse forventningene inneholder en forekomst av PhpSpec \ Matcher \ MatcherInterface at kamp() 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…  eller burde 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 implementerer MatcherInterface. 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 av MatcherInterface. 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 (), eller shouldNotReturn ()burde ikke være()shouldNotEqual () eller shouldNotBeEqualTo ().

Fra BasicMatcher, to metoder er arvet: positiveMatch () og negativeMatch (). 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 hvis fyrstikker() metode (abstrakt metode som kampførere må implementere) returnerer falsk. De negativeMatch () Metoden virker motsatt måte. De fyrstikker() 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 at fyrstikker() 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 av StdClass? La oss konsultere get_class () en gang til:

$ leverandør / bin / phpspec løpestreng (28) "PhpSpec \ Wrapper \ Collaborator" 

Nei. I stedet for StdClass vi får en forekomst av PhpSpec \ Wrapper \ Samarbeidspartner. Hva handler dette om?

Som Emnesamarbeidspartner er en wrapper og implementerer WrapperInterface. Det bryter en forekomst av\ Prophecy \ Prophecy \ ObjectProphecy, som stammer fra profetien, det mocking-rammene som kommer sammen med PhpSpec. I stedet for en StdClass 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 mot ekte 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.