Komme i gang med Phpspec

I denne korte, men omfattende, opplæringen, tar vi en titt på atferdsdrevet utvikling (BDD) med phpspec. For det meste vil det være en introduksjon til phpspec verktøyet, men når vi går, vil vi berøre ulike BDD konsepter. BDD er et hett tema i disse dager, og phpspec har nylig fått stor oppmerksomhet i PHP-fellesskapet.

SpecBDD & Phpspec

BDD handler om å beskrive oppførselen til programvare for å få designet riktig. Det er ofte forbundet med TDD, men mens TDD fokuserer på testing din søknad, BDD handler om beskriver sin oppførsel. Bruke en BDD tilnærming vil tvinge deg til å kontinuerlig vurdere de faktiske krav og ønsket oppførsel av programvaren du bygger.

To BDD-verktøy har fått stor oppmerksomhet i PHP-fellesskapet nylig, Behat og phpspec. Behat hjelper deg med å beskrive ytre atferd av søknaden din, ved hjelp av det lesbare Gherkin-språket. phpspec, derimot, hjelper deg med å beskrive intern oppførsel av søknaden din, ved å skrive små "spesifikasjoner" i PHP-språket - dermed SpecBDD. Disse spesifikasjonene tester at koden din har ønsket oppførsel.

Hva vi vil gjøre

I denne opplæringen dekker vi alt relatert til å komme i gang med phpspec. På vei vil vi bygge grunnlaget for et todo-listeprogram, trinnvis, ved hjelp av en SpecBDD-tilnærming. Når vi går, har vi phpspec på vei!

Merk: Dette er en mellomliggende artikkel om PHP. Jeg antar at du har en god forståelse av objektorientert PHP.

Installasjon

For denne opplæringen antar jeg at du har følgende ting oppe:

  • Et fungerende PHP-oppsett (min 5.3)
  • komponist

Installering av phpspec gjennom Composer er den enkleste måten. Alt du trenger å gjøre er å kjøre følgende kommando i en terminal:

$ komponent krever phpspec / phpspec Vennligst oppgi en versjonsbegrensning for phpspec / phpspec kravet: 2.0.*@dev

Dette vil gjøre en composer.json filen for deg og installer phpspec i en selger/ katalog.

For å sikre at alt fungerer, kjør phpspec og se at du får følgende utgang:

$ leverandør / bin / phpspec kjøre 0 spesifikasjoner 0 eksempler 0ms

konfigurasjon

Før vi starter, må vi gjøre litt konfigurasjon. Når phpspec kjører, ser det etter en YAML-fil som heter phpspec.yml. Siden vi skal sette vår kode i et navneområde, må vi sørge for at phpspec vet om dette. Også, mens vi er på det, la oss sørge for at våre spesifikasjoner ser bra ut og fine når vi kjører dem.

Gå videre og lag filen med følgende innhold:

formatter.name: vakre suiter: todo_suite: namespace: Petersuhm \ Todo

Det finnes mange andre konfigurasjonsalternativer, som du kan lese om i dokumentasjonen.

En annen ting vi trenger å gjøre er å fortelle Komponisten hvordan å autoload vår kode. phpspec vil bruke Composer's autoloader, så dette er nødvendig for at våre spesifikasjoner skal kunne kjøre.

Legg til et autoload element i composer.json fil som Composer laget for deg:

"krav": "phpspec / phpspec": "2.0.*@dev", "autoload": "psr-0": "Petersuhm \\ Todo": "src"

Løping komponist dump-autoload vil oppdatere autoloader etter denne endringen.

Vår første spesifikasjon

Nå er vi klare til å skrive vår første spesifikasjon. Vi starter med å beskrive en klasse som heter TaskCollection. Vi vil ha phpspec generere en spesiell klasse for oss ved å bruke beskrive kommando (eller alternativt den korte versjonen desc) .

$ vendor / bin / phpspec beskriver "Petersuhm \ Todo \ TaskCollection" $ leverandør / bin / phpspec løp Vil du at jeg skal opprette 'Petersuhm \ Todo \ TaskCollection' for deg? y

Så hva skjedde her? Først spurte vi phpspec om å lage en spesifikasjon for en TaskCollection. For det andre, vi kjørte vår spec suite og deretter phpspec auto tilbød å skape den faktiske TaskCollection klasse for oss. Kult, er det ikke?

Gå videre og kjør suiten igjen, og du vil se at vi allerede har et eksempel i vår spesifikasjon (vi ser et øyeblikk et eksempel):

$ leverandør / bin / phpspec kjøre Petersuhm \ Todo \ TaskCollection 10 ✔ er initialiserbar 1 spesifikasjoner 1 eksempler (1 bestått) 7ms

Fra denne utgangen kan vi se at TaskCollection er initialiserbar. Hva handler dette om? Ta en titt på specfilen som phpspec genereres, og det bør være klarere:

shouldHaveType ( 'Petersuhm \ Todo \ TaskCollection'); 

Frasen "initialiserbar" er avledet fra en funksjon som heter it_is_initializable () hvilken phpspec har lagt til en klasse som heter TaskCollectionSpec. Denne funksjonen er hva vi refererer til som en eksempel. I dette spesielle eksempel har vi det vi refererer til som en Matcher kalt shouldHaveType () som sjekker typen av vår TaskCollection. Hvis du endrer parameteren som sendes til denne funksjonen til noe annet og kjører spesifikasjonen igjen, vil du se at den vil mislykkes. Før jeg forstår dette, tror jeg vi må undersøke i hvilken variabel $ dette refererer til i vår spesifikasjon.

Hva er $ dette?

Selvfølgelig, $ dette refererer til forekomsten av klassen TaskCollectionSpec, siden dette er bare vanlig PHP-kode. Men med phpspec må du behandle $ dette forskjellig fra hva du vanligvis gjør, siden under hetten, er det faktisk refererer til objektet under test, som faktisk er TaskCollection klasse. Denne oppførselen er arvet fra klassen ObjectBehavior, som sørger for at funksjonsanrop er proxied til den spesielle klassen. Dette betyr at SomeClassSpec vil proxy-metoden ringe til en forekomst av SomeClass. phpspec vil pakke inn disse metallsamtaler for å kjøre sine returverdier mot matchere som den du nettopp så.

Du trenger ikke en dyp forståelse av dette for å kunne bruke phpspec, husk det bare så langt du er bekymret, $ dette faktisk refererer til objektet under test.

Bygg vår oppgave samling

Så langt har vi ikke gjort noe selv. Men phpspec har gjort en tom TaskCollection klasse for oss å bruke. Nå er det på tide å fylle ut noen kode og gjøre denne klassen nyttig. Vi legger til to metoder: an Legg til() metode, for å legge til oppgaver, og a telle() metode, for å telle antall oppgaver i samlingen.

Legge til en oppgave

Før vi skriver noen ekte kode, bør vi skrive et eksempel i vår spesifikasjon. I vårt eksempel vil vi prøve å legge til en oppgave i samlingen, og deretter etterpå sørge for at oppgaven faktisk er lagt til. For å gjøre dette trenger vi en forekomst av (hittil ikke eksisterende) Oppgave klasse. Hvis vi legger til denne avhengigheten som en parameter til vår spesifikke funksjon, vil phpspec automatisk gi oss en forekomst som vi kan bruke. Faktisk er forekomsten ikke en ekte forekomst, men hva phpspec refererer til som a samarbeidspartner. Dette objektet vil fungere som det virkelige objektet, men phpspec tillater oss å gjøre mer fancy ting med dette, som vi ser snart. Selv om Oppgave klasse eksisterer ikke ennå, for nå, bare late som det gjør. Åpne opp TaskCollectionSpec og legg til en bruk uttalelse for Oppgave klasse og legg til eksempelet it_adds_a_task_to_the_collection ():

bruk Petersuhm \ Todo \ Oppgave; ... funksjon it_adds_a_task_to_the_collection (Oppgave $ oppgave) $ this-> add ($ task); $ Dette-> oppgaver [0] -> shouldbe ($ oppgave); 

I vårt eksempel skriver vi koden "vi skulle ønske vi hadde". Vi kaller Legg til() metode og deretter prøve å gi den en $ oppgave. Deretter kontrollerer vi at oppgaven faktisk ble lagt til instansvariabelen $ oppgaver. Matcheren bør være() er en identitet matcher som ligner på PHP === komparator. Du kan også bruke bør være(), shouldBeEqualTo (), shouldEqual () eller shouldReturn () - de alle gjør det samme.

Kjører phpspec vil gi noen feil, siden vi ikke har en klasse som heter Oppgave ennå.

La oss få phpspec fikse det for oss:

$ leverandør / bin / phpspec beskriver "Petersuhm \ Todo \ Oppgave" $ leverandør / bin / phpspec løp Vil du at jeg skal lage 'Petersuhm \ Todo \ Task' for deg? y

Kjører phpspec igjen, skjer noe interessant:

$ vendor / bin / phpspec løp Vil du at jeg skal opprette 'Petersuhm \ Todo \ TaskCollection :: add ()' for deg? y

Perfekt! Hvis du tar en titt på TaskCollection.php fil, vil du se at phpspec har gjort en Legg til() funksjon for oss å fylle ut:

phpspec klager fortsatt, skjønt. Vi har ikke en $ oppgaver array, så la oss lage en og legge til oppgaven med den:

oppgaver [] = $ oppgave; 

Nå er våre spesifikasjoner alle fine og grønne. Merk at jeg sørget for å skrive inn $ oppgave parameter.

Bare for å sikre at vi fikk det riktig, la oss legge til en annen oppgave:

funksjon it_adds_a_task_to_the_collection (Oppgave $ oppgave, Oppgave $ anotherTask) $ this-> add ($ task); $ Dette-> oppgaver [0] -> shouldbe ($ oppgave); $ Dette-> legge til ($ anotherTask); $ Dette-> oppgaver [1] -> shouldbe ($ anotherTask); 

Kjører phpspec, det ser ut til at vi er alle gode.

Gjennomføring av Tellbart Interface

Vi ønsker å vite hvor mange oppgaver som er i en samling, noe som er en god grunn til å bruke en av grensesnittene fra Standard PHP Library (SPL), nemlig Tellbart grensesnitt. Dette grensesnittet dikterer at en klasse implementerer den ha en telle() metode.

Tidligere brukte vi matcheren shouldHaveType (), hvilken er en type Matcher. Den bruker PHP komparatoren tilfelle av å validere at et objekt faktisk er en forekomst av en gitt klasse. Det finnes 4 type-matchere, som alle gjør det samme. En av dem er shouldImplement (), som er perfekt for vårt formål, så la oss gå videre og bruke det i et eksempel:

funksjon it_is_countable () $ this-> shouldImplement ('Countable'); 

Se hvor vakkert det leser? La oss kjøre eksemplet og få phpspec til å lede veien for oss:

$ leverandør / bin / phpspec løp Petersuhm / Todo / TaskCollection 25 ✘ er tellbart forventet en forekomst av Countable, men fikk [obj: Petersuhm \ Todo \ TaskCollection].

Ok, så vår klasse er ikke en forekomst av Tellbart siden vi ikke har implementert det ennå. La oss oppdatere koden for vår TaskCollection klasse:

klasse TaskCollection implementerer \ Countable

Våre tester vil ikke løpe, siden Tellbart grensesnittet har en abstrakt metode, telle(), som vi må implementere. En tom metode vil gjøre kunsten for nå:

offentlig funksjonstelling () // ...

Og vi er tilbake til grønt. For øyeblikket vår telle() Metoden gjør ikke mye, og det er faktisk ganske ubrukelig. La oss skrive en spesifikasjon for atferden vi ønsker det. For det første, uten oppgaver, forventes tellefunksjonen å returnere null:

fungere it_counts_elements_of_the_collection () $ this-> count () -> shouldReturn (0); 

Det kommer tilbake null, ikke 0. For å få en grønn test, la oss fikse dette på TDD / BDD-måten:

offentlig funksjonstelling () return 0; 

Vi er grønne og alt er bra, men dette er trolig ikke oppførselen vi ønsker. I stedet la vi utvide vår spesifikke og legge til noe i $ oppgaver matrise:

fungere it_counts_elements_of_the_collection () $ this-> count () -> shouldReturn (0); $ this-> tasks = ['foo']; $ Dette-> count () -> shouldReturn (1); 

Selvfølgelig er vår kode fortsatt tilbake 0, og vi har et rødt skritt. Å fikse dette er ikke så vanskelig og vår TaskCollection klassen skal nå se slik ut:

oppgaver [] = $ oppgave;  offentlig funksjonstelling () return count ($ this-> tasks); 

Vi har en grønn test og vår telle() metoden fungerer. For en dag!

Forventninger og løfter

Husk jeg fortalte deg at phpspec lar deg lage kule ting med forekomster av samarbeidspartner klasse, AKA de tilfellene som automatisk injiseres av phpspec? Hvis du har skrevet enhetstester før, vet du hva mocks og stubber er. Hvis du ikke gjør det, må du ikke bekymre deg for mye om det. Det er bare sjargong. Disse tingene refererer til "falske" objekter som vil fungere som dine virkelige objekter, men lar deg teste isolert. phpspec vil automatisk slå disse samarbeidspartner forekommer i mocks og stubber hvis du trenger det i dine spesifikasjoner. 

Dette er virkelig fantastisk. Under hetten bruker phpspec Profesjonsbiblioteket, som er et høyt uttalt mocking-rammeverk som spiller godt med phpspec (og er bygd av de samme fantastiske folkene). Du kan sette en forventning på en samarbeidspartner (mocking), som "denne metoden skal kalles", og du kan legge til løfter (stubbing), som" denne metoden vil returnere denne verdien ". Med phpspec er dette veldig enkelt, og vi skal gjøre begge to.

La oss lage en klasse, vi ringer det Gjøremålsliste, som kan gjøre bruk av vår samlingsklasse.

$ vendor / bin / phpspec desc "Petersuhm \ Todo \ TodoList" $ leverandør / bin / phpspec løp Vil du at jeg skal lage 'Petersuhm \ Todo \ TodoList' for deg? y

Legge til oppgaver

Det første eksemplet vi legger til, er en for å legge til oppgaver. Vi skal lage en addTask () metode, det gjør ingenting annet enn å legge til en oppgave i vår samling. Det rett og slett leder anropet til Legg til() metode på samlingen, så dette er et perfekt sted å gjøre bruk av en forventning. Vi vil ikke at metoden faktisk skal ringe Legg til() metode, vi vil bare sørge for at den prøver å gjøre det. Videre vil vi sørge for at det bare kaller det en gang. Ta en titt på hvordan vi kan gå om dette med phpspec:

shouldHaveType ( 'Petersuhm \ Todo \ todolist');  funksjon it_adds_a_task_to_the_list (TaskCollection $ oppgaver, Oppgave $ oppgave) $ tasks-> add ($ task) -> shouldBeCalledTimes (1); $ this-> tasks = $ tasks; $ Dette-> addTask ($ oppgave); 

For det første har vi phpspec gi oss de to samarbeidspartnene vi trenger: en oppgave og en oppgave. Deretter setter vi en forventning på samarbeidspartneren for oppgaver som i utgangspunktet sier: "The Legg til() Metoden skal kalles nøyaktig 1 gang med variabelen $ oppgave som en parameter ". Slik forbereder vi samarbeidspartneren vår, som nå er en mock, før vi tildeler den til $ oppgaver eiendom på Gjøremålsliste. Til slutt prøver vi faktisk å ringe addTask () metode.

Ok, hva har phpspec å si om dette:

$ leverandør / bin / phpspec kjøre Petersuhm / Todo / TodoList 17! legger til en oppgave i listen eiendomsoppgaver ikke funnet.

De $ oppgaver Eiendommen er ikke eksisterende - enkelt en:

Prøv igjen, og har phpspec guide vår måte:

$ leverandør / bin / phpspec løp Vil du at jeg skal lage 'Petersuhm \ Todo \ TodoList :: addTask ()' for deg? y $ vendor / bin / phpspec kjører Petersuhm / Todo / TodoList 17 ✘ legger til en oppgave i listen noen forutsetninger mislyktes: Double \ Petersuhm \ Todo \ TaskCollection \ P4: Forventet nøyaktig 1 samtaler som samsvarer: Double \ Petersuhm \ Todo \ TaskCollection \ P4-> legg til (eksakt (Double \ Petersuhm \ Todo \ Oppgave \ P3: 000000002544d76d0000000059fcae53)) men ingen ble gjort.

Ok, nå skjedde noe interessant. Se meldingen "Forventet nøyaktig 1 samtaler som stemmer overens: ..."? Dette er vår sviktende forventning. Dette skjer fordi etter å ha ringt addTask () metode, den Legg til() Metoden på samlingen var ikke kalt, som vi forventet å være.

For å komme tilbake til grønt, fyll inn følgende kode i tomt addTask () metode:

Oppgaver> legge til ($ oppgave); 

Tilbake til grønt! Det føles bra, ikke sant?

Sjekker etter oppgaver

La oss også se på løfter. Vi ønsker en metode som kan fortelle oss om det er noen oppgaver i samlingen. For dette vil vi bare sjekke returverdien til telle() metode på samlingen. Igjen, vi trenger ikke en ekte forekomst med en ekte telle() metode. Vi trenger bare å forsikre oss om at vår kode kaller noe telle() metode og gjøre noen ting avhengig av returverdi.

Ta en titt på følgende eksempel:

funksjon it_checks_whether_it_has_any_tasks (TaskCollection $ oppgaver) $ tasks-> count () -> willReturn (0); $ this-> tasks = $ tasks; $ Dette-> hasTasks () -> shouldReturn (false); 

Vi har en samarbeidsprosjekt med samarbeidspartner som har en telle() metode som vil returnere null. Dette er vårt løfte. Hva dette betyr er at hver gang noen ringer telle() metode, vil det returnere null. Vi tilordner den forberedte samarbeidspartneren til $ oppgaver eiendom på vårt objekt. Til slutt prøver vi å kalle en metode, hasTasks (), og sørg for at den kommer tilbake falsk.

Hva har phspec å si om dette?

$ leverandør / bin / phpspec løp Vil du at jeg skal lage 'Petersuhm \ Todo \ TodoList :: hasTasks ()' for deg? y $ leverandør / bin / phpspec kjøre Petersuhm / Todo / TodoList 25 ✘ sjekker om det har noen oppgaver forventet falskt, men ble null.

Kul. phpspec gjorde oss en hasTasks () metode og ikke overraskende, det returnerer null, ikke falsk.

Igjen, dette er en enkel å fikse:

offentlig funksjon harTasks () return false; 

Vi er tilbake til grønt, men dette er ikke helt det vi ønsker. La oss se etter oppgaver når det er 20 av dem. Dette bør returnere ekte:

funksjon it_checks_whether_it_has_any_tasks (TaskCollection $ oppgaver) $ tasks-> count () -> willReturn (0); $ this-> tasks = $ tasks; $ Dette-> hasTasks () -> shouldReturn (false); $ Oppgaver> count () -> willreturn (20); $ this-> tasks = $ tasks; $ Dette-> hasTasks () -> shouldReturn (true); 

Kjør phspec og vi får:

$ leverandør / bin / phpspec kjøre Petersuhm / Todo / TodoList 25 ✘ sjekker om det er noen oppgaver som forventes sanne, men har falsk.

Greit, falsk er ikke ekte, så vi må forbedre koden vår. La oss bruke det telle() metode for å se om det er oppgaver eller ikke:

offentlig funksjon harTasks () if ($ this-> tasks-> count ()> 0) return true; returner falsk; 

Tah Dah! Tilbake til grønt!

Bygg Custom Matchers

En del av å skrive gode spesifikasjoner er å gjøre dem så lesbare som mulig. Vårt siste eksempel kan faktisk bli forbedret en liten bit, takket være phpspecs tilpassede matchere. Det er enkelt å implementere tilpassede kampanjer - alt vi trenger å gjøre er å overskrive getMatchers () metode som er arvet fra ObjectBehavior. Ved å implementere to tilpassede kampanjer, kan vår spesifikasjon endres slik at den ser slik ut:

funksjon it_checks_whether_it_has_any_tasks (TaskCollection $ oppgaver) $ tasks-> count () -> willReturn (0); $ this-> tasks = $ tasks; $ Dette-> hasTasks () -> shouldBeFalse (); $ Oppgaver> count () -> willreturn (20); $ this-> tasks = $ tasks; $ Dette-> hasTasks () -> shouldBeTrue ();  funksjon getMatchers () return ['beTrue' => funksjon ($ subject) return $ subject === true; , 'beFalse' => funksjon ($ emne) return $ subject === false; ,]; 

Jeg synes dette ser ganske bra ut. Husk at refactoring dine spesifikasjoner er viktig for å holde dem oppdatert. Implementering av dine egne tilpassede kampanjer kan rydde opp spesifikasjonene dine og gjøre dem mer lesbare.

Faktisk kan vi også bruke negativt av matcherne:

funksjon it_checks_whether_it_has_any_tasks (TaskCollection $ oppgaver) $ tasks-> count () -> willReturn (0); $ this-> tasks = $ tasks; $ Dette-> hasTasks () -> shouldNotBeTrue (); $ Oppgaver> count () -> willreturn (20); $ this-> tasks = $ tasks; $ Dette-> hasTasks () -> shouldNotBeFalse (); 

Yeah. Ganske kult!

Konklusjon

Alle våre spesifikasjoner er grønne og ser på hvor fint de dokumenterer vår kode!

 Petersuhm \ Todo \ TaskCollection 10 ✔ er initialiserbar 15 ✔ legger til en oppgave i samlingen 24 ✔ er tellbar 29 ✔ teller elementer i samlingen Petersuhm \ Todo \ Oppgave 10 ✔ er initialiserbar Petersuhm \ Todo \ TodoList 11 ✔ er initialiserbar 16 ✔ legger til en oppgave til listen 24 ✔ sjekker om det har noen oppgaver 3 spesifikasjoner 8 eksempler (8 bestått) 16ms

Vi har effektivt beskrevet og oppnådd ønsket oppførsel av koden vår. For ikke å nevne, er vår kode 100% dekket av våre spesifikasjoner, noe som betyr at refactoring ikke vil være en fryktinduserende opplevelse.

Ved å følge med, håper jeg du ble inspirert til å gi phpspec et forsøk. Det er mer enn et testverktøy - det er et designverktøy. Når du er vant til å bruke phpspec (og dens fantastiske kodegenereringsverktøy), har du det vanskelig å la det gå igjen! Folk klager ofte på at TDD eller BDD reduserer dem. Etter å ha inkorporert phpspec i arbeidsprosessen, føler jeg meg virkelig den motsatte måten - produktiviteten min er betydelig forbedret. Og koden min er mer solid!