I denna korta, men ändå omfattande, handledning, kommer vi att titta på beteendestyrd utveckling (BDD) med phpspec. För det mesta blir det en introduktion till phpspec-verktyget, men när vi går kommer vi att beröra olika BDD-koncept. BDD är ett hett ämne idag och phpspec har nyligen fått stor uppmärksamhet i PHP-community.
BDD handlar om att beskriva mjukvarans beteende för att få utformningen rätt. Det är ofta associerat med TDD, men TDD fokuserar på testning din ansökan, BDD handlar mer om beskriver sitt beteende. Att använda ett BDD-tillvägagångssätt kommer att tvinga dig att ständigt överväga de faktiska kraven och önskat beteende hos den programvara du bygger.
Två BDD-verktyg har fått stor uppmärksamhet i PHP-communityen nyligen, Behat och phpspec. Behat hjälper dig att beskriva extern beteende av din ansökan, med hjälp av det läsbara Gherkinspråket. phpspec, å andra sidan, hjälper dig att beskriva interna beteendet av din ansökan, genom att skriva små "specs" i PHP-språket - alltså SpecBDD. Dessa specifikationer testar att din kod har önskat beteende.
I denna handledning täcker vi allt som är relaterat till att komma igång med phpspec. På väg kommer vi att bygga grunden för en todo list-applikation, steg för steg, med hjälp av en SpecBDD-metod. När vi går har vi phpspec på vägen!
Obs! Det här är en mellanliggande artikel om PHP. Jag antar att du har ett bra grepp om objektorienterad PHP.
För denna handledning antar jag att du har följande saker igång:
Installera phpspec genom Composer är det enklaste sättet. Allt du behöver göra är att köra följande kommando i en terminal:
$ komponent kräver phpspec / phpspec Vänligen ange en version begränsning för phpspec / phpspec krav: 2.0.*@dev
Detta kommer att göra en composer.json
filen för dig och installera phpspec i en Säljare/
katalog.
För att se till att allt fungerar, springa phpspec
och se till att du får följande utdata:
$ leverantör / bin / phpspec köra 0 specifikationer 0 exempel 0ms
Innan vi börjar måste vi göra lite konfiguration. När phpspec körs letar den efter en YAML-fil som heter phpspec.yml
. Eftersom vi kommer att sätta vår kod i ett namnrymd måste vi se till att phpspec vet om detta. Även när vi är på det, låt oss se till att våra specifikationer ser snygga och vackra ut när vi kör dem.
Fortsätt och gör filen med följande innehåll:
formatter.name: vackra sviter: todo_suite: namespace: Petersuhm \ Todo
Det finns många andra konfigurationsalternativ som du kan läsa om i dokumentationen.
En annan sak som vi behöver göra är att berätta för Kompositören hur du ska autoload vår kod. phpspec kommer att använda kompositörens autoloader, så detta krävs för att våra specifikationer ska kunna köras.
Lägg till ett autoload element till composer.json
fil som kompositören gjorde för dig:
"krav": "phpspec / phpspec": "2.0.*@dev", "autoload": "psr-0": "Petersuhm \\ Todo": "src"
Löpning kompositör dump-autoload
kommer att uppdatera autoloader efter denna ändring.
Nu är vi redo att skriva vår första spec. Vi börjar med att beskriva en klass som heter TaskCollection
. Vi får phpspec generera en spec klass för oss genom att använda beskriva
kommando (eller alternativt den korta versionen desc
) .
$ vendor / bin / phpspec beskriva "Petersuhm \ Todo \ TaskCollection" $ säljare / bin / phpspec-kör Vill du att jag ska skapa 'Petersuhm \ Todo \ TaskCollection' för dig? y
Så vad hände här? Först frågade vi phpspec att skapa en spec för a TaskCollection
. För det andra körde vi vår spec-svit och sedan phpspec auto erbjuds att skapa själva TaskCollection
klass för oss. Coolt, är det inte?
Gå vidare och springa sviten igen, så ser du att vi redan har ett exempel i vår spec (vi får se ett exempel på ett exempel):
$ vendor / bin / phpspec kör Petersuhm \ Todo \ TaskCollection 10 ✔ är initialiserbar 1 specifikationer 1 exempel (1 godkänd) 7ms
Från denna produktion kan vi se att TaskCollection
är initierbar. Vad handlar det här om? Ta en titt på specfilen som phpspec genererade, och det borde vara tydligare:
shouldHaveType ( 'Petersuhm \ Todo \ TaskCollection');
Uttrycket "är initialiserbart" härrör från en funktion som heter it_is_initializable ()
vilken phpspec har lagt till en klass som heter TaskCollectionSpec
. Denna funktion är vad vi hänvisar till som en exempel. I det här exemplet har vi vad vi hänvisar till som en passare kallad shouldHaveType ()
som kontrollerar typen av vår TaskCollection
. Om du ändrar parametern som skickas till den här funktionen till något annat och kör specen igen så ser du att den kommer att misslyckas. Innan jag förstår detta, tror jag att vi behöver undersöka i vilken variabel $ detta
hänvisar till i vår spec.
$ detta
?Självklart, $ detta
hänvisar till förekomsten av klassen TaskCollectionSpec
, eftersom detta bara är vanlig PHP-kod. Men med phpspec måste du behandla $ detta
annorlunda än vad du normalt gör, eftersom det ligger under huven, det refererar faktiskt till objektet som testas, vilket i själva verket är det TaskCollection
klass. Detta beteende är ärft från klassen ObjectBehavior
, vilket säkerställer att funktionssamtal är proxied till den specifika klassen. Detta innebär att SomeClassSpec
Kommer proxymetoden att ringa till en instans av SomeClass
. phpspec kommer att sätta in dessa metodsamtal för att kunna köra sina returvärden mot matchare som den du just såg.
Du behöver inte en djup förståelse för detta för att kunna använda phpspec, kom bara ihåg det så långt du är bekymrad, $ detta
refererar faktiskt till objektet som testas.
Hittills har vi inte gjort någonting själv. Men phpspec har gjort en tom TaskCollection
klass för oss att använda. Nu är det dags att fylla i någon kod och göra den här klassen användbar. Vi lägger till två metoder: an Lägg till()
metod, för att lägga till uppgifter och a räkna()
metod, för att räkna antalet uppgifter i samlingen.
Innan vi skriver någon riktig kod, borde vi skriva ett exempel i vår spec. I vårt exempel vill vi försöka lägga till en uppgift i samlingen, och sedan se till att uppgiften faktiskt läggs till. För att göra detta behöver vi en förekomst av (hittills inte existerande) Uppgift
klass. Om vi lägger till detta beroende som en parameter till vår spec-funktion, kommer phpspec automatiskt att ge oss en instans som vi kan använda. Egentligen är förekomsten inte en riktig instans, men vad phpspec refererar till som a Medarbetare
. Detta objekt kommer att fungera som det verkliga objektet, men phpspec tillåter oss att göra mer fina saker med det här, vilket vi snart ser. Även om Uppgift
klass existerar inte än, för nu, låtsas som det gör. Öppna upp TaskCollectionSpec
och lägg till en använda sig av
uttalande för Uppgift
klass och lägg sedan till exemplet it_adds_a_task_to_the_collection ()
:
använd Petersuhm \ Todo \ Task; ... funktion it_adds_a_task_to_the_collection (Uppgift $ uppgift) $ this-> add ($ task); $ This-> uppgifter [0] -> shouldbe ($ uppgift);
I vårt exempel skriver vi koden "vi önskar att vi hade". Vi kallar Lägg till()
metod och försök sedan ge det en $ uppgift
. Då kontrollerar vi att uppgiften faktiskt fanns till instansvariabeln $ uppgifter
. Matcharen borde vara()
är en identitet matcher som liknar PHP ===
komparator. Du kan använda antingen borde vara()
, shouldBeEqualTo ()
, shouldEqual ()
eller shouldReturn ()
- de alla gör detsamma.
Running phpspec kommer att ge några fel, eftersom vi inte har en klass som heter Uppgift
än.
Låt oss få phpspec fixa det för oss:
$ vendor / bin / phpspec beskriva "Petersuhm \ Todo \ Task" $ säljare / bin / phpspec-kör Vill du att jag ska skapa "Petersuhm \ Todo \ Task" för dig? y
Running phpspec igen, något intressant händer:
$ vendor / bin / phpspec run Vill du att jag ska skapa 'Petersuhm \ Todo \ TaskCollection :: add ()' för dig? y
Perfekt! Om du tittar på TaskCollection.php
fil, så kommer du att se att phpspec har gjort en Lägg till()
funktion för oss att fylla i:
phpspec klagar dock fortfarande. Vi har inte en
$ uppgifter
array, så låt oss göra en och lägga till uppgiften för den:uppgifter [] = $ uppgift;Nu är våra specs alla trevliga och gröna. Observera att jag var säker på att typhint av
$ uppgift
parameter.Bara för att se till att vi har rätt, låt oss lägga till en annan uppgift:
funktion it_adds_a_task_to_the_collection (Uppgift $ uppgift, Uppgift $ anotherTask) $ this-> add ($ task); $ This-> uppgifter [0] -> shouldbe ($ uppgift); $ This-> lägga ($ anotherTask); $ This-> uppgifter [1] -> shouldbe ($ anotherTask);Running phpspec, det verkar som om vi är alla bra.
Genomförande av
SOM GÅR ATT RÄKNA
GränssnittVi vill veta hur många uppgifter som finns i en samling, vilket är en stor anledning att använda ett av gränssnitten från Standard PHP Library (SPL), nämligen
SOM GÅR ATT RÄKNA
gränssnitt. Detta gränssnitt dikterar att en klass genomför det måste ha enräkna()
metod.Tidigare använde vi matcharen
shouldHaveType ()
, Vilket är en typ passare. Det använder PHP komparatorninstans av
att validera att ett objekt faktiskt är en förekomst av en given klass. Det finns 4 typer matchare, som alla gör detsamma. En av dem ärshouldImplement ()
, vilket är perfekt för vårt syfte, så låt oss gå vidare och använda det i ett exempel:funktion it_is_countable () $ this-> shouldImplement ('Countable');Se hur vacker det läser? Låt oss köra exemplet och ha phpspec leda vägen för oss:
$ leverantör / bin / phpspec kör Petersuhm / Todo / TaskCollection 25 ✘ är beräknas förväntas vara en förekomst av Countable, men fick [obj: Petersuhm \ Todo \ TaskCollection].Okej, så vår klass är inte en förekomst av
SOM GÅR ATT RÄKNA
eftersom vi inte har implementerat det än. Låt oss uppdatera koden för vårTaskCollection
klass:klass TaskCollection implementerar \ CountableVåra test kommer inte att springa, eftersom
SOM GÅR ATT RÄKNA
gränssnittet har en abstrakt metod,räkna()
, som vi måste genomföra. En tom metod kommer att göra tricket för nu:allmänfunktionsräkning () // ...Och vi är tillbaka till grönt. För tillfället vårt
räkna()
Metoden gör inte mycket, och det är faktiskt ganska meningslöst. Låt oss skriva en specifikation för beteendet som vi önskar att det ska ha. Först utan några uppgifter förväntas vår räknefunktion returnera noll:funktion it_counts_elements_of_the_collection () $ this-> count () -> shouldReturn (0);Det återvänder
null
, inte0
. För att få ett grönt test, låt oss fixa detta på TDD / BDD-sättet:allmänfunktionsräkning () return 0;Vi är gröna och allt är bra, men det är nog inte det beteende vi vill ha. Låt oss istället expandera vår spec och lägga till något till
$ uppgifter
array:funktion it_counts_elements_of_the_collection () $ this-> count () -> shouldReturn (0); $ this-> tasks = ['foo']; $ This-> count () -> shouldReturn (1);Naturligtvis återkommer vår kod fortfarande
0
, och vi har ett rött steg. Att fixa detta är inte för svårt och vårtTaskCollection
klassen ska nu se ut så här:uppgifter [] = $ uppgift; allmänfunktionsräkning () return count ($ this-> tasks);Vi har ett grönt test och vårt
räkna()
metod fungerar. Vilken dag!Förväntningar och löften
Kom ihåg att jag sa att phpspec kan du göra coola saker med instanser av
Medarbetare
klass, AKA de instanser som automatiskt injiceras av phpspec? Om du har skrivit enhetsprov innan vet du vad mocks och stubbar är. Om du inte gör det, oroa dig inte för mycket om det. Det är bara jargong. Dessa saker avser "falska" objekt som kommer att fungera som dina verkliga objekt, men låter dig testa i isolation. phpspec kommer automatiskt att vrida dessaMedarbetare
instanser i mocks och stubbar om du behöver det i dina specifikationer.Det här är verkligen fantastiskt. Under huven använder phpspec profetianbiblioteket, vilket är en mycket uppenbar mockingram som spelar bra med phpspec (och bygger av samma fantastiska folk). Du kan ställa en förväntan på en samarbetspartner (mocking), som "den här metoden ska kallas", och du kan lägga till löften (stubbing), som" den här metoden kommer att återvända detta värde ". Med phpspec är det här väldigt enkelt och vi ska göra båda sakerna nästa.
Låt oss göra en klass, vi ringer det
Att göra lista
, som kan utnyttja vår samlingsklass.$ vendor / bin / phpspec desc "Petersuhm \ Todo \ TodoList" $ säljare / bin / phpspec run Vill du att jag ska skapa 'Petersuhm \ Todo \ TodoList' för dig? yLägga till uppgifter
Det första exemplet vi lägger till är en för att lägga till uppgifter. Vi ska göra en
addTask ()
metod, det gör inget annat än att lägga till en uppgift i vår samling. Det leder helt enkelt samtalet tillLägg till()
metod på insamlingen, så det här är en perfekt plats att utnyttja en förväntan. Vi vill inte att metoden faktiskt ska ringaLägg till()
metod, vi vill bara se till att det försöker göra det. Dessutom vill vi se till att det bara kallas det en gång. Ta en titt på hur vi kan hantera detta med phpspec:shouldHaveType ( 'Petersuhm \ Todo \ todolist'); funktion it_adds_a_task_to_the_list (TaskCollection $ uppgifter, Uppgift $ uppgift) $ tasks-> add ($ task) -> shouldBeCalledTimes (1); $ this-> tasks = $ tasks; $ This-> addTask ($ uppgift);För det första har vi phpspec förser oss med de två samarbetarna vi behöver: en arbetsuppgift och en uppgift. Sedan ställer vi en förväntan på samarbetaren för uppgiftsinsamling som i grunden säger: "
Lägg till()
Metoden ska kallas exakt 1 gång med variabeln$ uppgift
som en parameter ". Så här förbereder vi vår samarbetspartner, som nu är en mock, innan vi tilldelar den till$ uppgifter
egendom påAtt göra lista
. Slutligen försöker vi faktiskt kallaaddTask ()
metod.Ok, vad har phpspec att säga om detta:
$ säljare / bin / phpspec kör Petersuhm / Todo / TodoList 17! lägger till en uppgift i listuppgifterna som inte hittats.De
$ uppgifter
egendom är existerande - lätt en:Försök igen och har phpspec guide vår väg:
$ vendor / bin / phpspec run Vill du att jag ska skapa 'Petersuhm \ Todo \ TodoList :: addTask ()' för dig? y $ vendor / bin / phpspec kör Petersuhm / Todo / TodoList 17 ✘ lägger till en uppgift i listan några förutspådningar misslyckades: Double \ Petersuhm \ Todo \ TaskCollection \ P4: Förväntat exakt 1 samtal som matchar: Double \ Petersuhm \ Todo \ TaskCollection \ P4-> lägg till (exakt (Double \ Petersuhm \ Todo \ Task \ P3: 000000002544d76d0000000059fcae53)) men ingen gjordes.Okej, nu hände något intressant. Se meddelandet "Förväntat exakt 1 samtal som matchar: ..."? Detta är vår felaktiga förväntan. Detta händer för att efter att ha ringt
addTask ()
metod, denLägg till()
Metoden på insamlingen var inte kallas, vilket vi förväntade oss att vara.För att komma tillbaka till grön fyller du i följande kod i den tomma
addTask ()
metod:för Åtgärder> Lägg ($ uppgift);Tillbaka till grönt! Det känns bra, rätt?
Kollar efter uppdrag
Låt oss ta en titt på löften också. Vi vill ha en metod som kan berätta om det finns några uppgifter i samlingen. För detta ska vi helt enkelt kontrollera avkastningsvärdet för
räkna()
metod på insamlingen. Återigen behöver vi inte en riktig förekomst med en riktigräkna()
metod. Vi behöver bara se till att vår kod kallar literäkna()
metod och gör vissa saker beroende på returvärdet.Ta en titt på följande exempel:
funktion it_checks_whether_it_has_any_tasks (TaskCollection $ tasks) $ tasks-> count () -> willReturn (0); $ this-> tasks = $ tasks; $ This-> hasTasks () -> shouldReturn (false);Vi har en samarbetspartner för uppgiftsinsamling som har en
räkna()
metod som kommer att återvända noll. Detta är vårt löfte. Vad detta betyder är att varje gång någon ringerräkna()
metod kommer det att returnera noll. Vi tilldelar sedan den förberedda samarbetaren till$ uppgifter
egendom på vårt objekt. Slutligen försöker vi kalla en metod,hasTasks ()
, och se till att den återvänderfalsk
.Vad har phspec att säga om detta?
$ vendor / bin / phpspec run Vill du att jag ska skapa 'Petersuhm \ Todo \ TodoList :: hasTasks ()' för dig? y $ leverantör / bin / phpspec kör Petersuhm / Todo / TodoList 25 ✘ kontrollerar om det är några uppgifter som förväntas falska men har blivit null.Häftigt. phpspec gjorde oss en
hasTasks ()
metod och inte överraskande, det återvändernull
, intefalsk
.Återigen är det här enkelt att fixa:
allmän funktion harTasks () return false;Vi är tillbaka till grönt, men det är inte riktigt vad vi vill ha. Låt oss söka efter uppgifter när det finns 20 av dem. Detta borde återvända
Sann
:funktion it_checks_whether_it_has_any_tasks (TaskCollection $ tasks) $ tasks-> count () -> willReturn (0); $ this-> tasks = $ tasks; $ This-> hasTasks () -> shouldReturn (false); $ För Åtgärder> count () -> willreturn (20); $ this-> tasks = $ tasks; $ This-> hasTasks () -> shouldReturn (true);Kör phspec och vi får:
$ vendor / bin / phpspec kör Petersuhm / Todo / TodoList 25 ✘ kontrollerar om det är något som förväntas vara sannt, men har falskt.Okej,
falsk
är inteSann
, så vi behöver förbättra vår kod. Låt oss använda deträkna()
metod för att se om det finns uppgifter eller inte:allmän funktion harTasks () om ($ this-> tasks-> count ()> 0) returnera true; returnera false;Tah dah! Tillbaka till grönt!
Bygga anpassade matchare
En del av att skriva bra specifikationer är att göra dem så läsliga som möjligt. Vårt senaste exempel kan faktiskt förbättras en liten bit tack vare phpspecs anpassade matchare. Det är enkelt att implementera anpassade matchare - allt vi behöver göra är att skriva över
getMatchers ()
metod som är ärvt frånObjectBehavior
. Genom att implementera två anpassade matchare kan vår specifikation ändras så att den ser ut så här:funktion it_checks_whether_it_has_any_tasks (TaskCollection $ tasks) $ tasks-> count () -> willReturn (0); $ this-> tasks = $ tasks; $ This-> hasTasks () -> shouldBeFalse (); $ För Åtgärder> count () -> willreturn (20); $ this-> tasks = $ tasks; $ This-> hasTasks () -> shouldBeTrue (); funktion getMatchers () return ['beTrue' => funktion ($ subject) return $ subject === true; , 'beFalse' => funktion ($ subject) return $ subject === false; ,];Jag tycker att det ser ganska bra ut. Kom ihåg att refactoring av dina specifikationer är viktigt för att hålla dem uppdaterade. Genomförandet av egna anpassade matchare kan städa upp dina specifikationer och göra dem mer läsbara.
Faktum är att vi kan använda negativet av matcharna också:
funktion it_checks_whether_it_has_any_tasks (TaskCollection $ tasks) $ tasks-> count () -> willReturn (0); $ this-> tasks = $ tasks; $ This-> hasTasks () -> shouldNotBeTrue (); $ För Åtgärder> count () -> willreturn (20); $ this-> tasks = $ tasks; $ This-> hasTasks () -> shouldNotBeFalse ();Ja. Ganska cool!
Slutsats
Alla våra specifikationer är gröna och titta på hur trevligt de dokumenterar vår kod!
Petersuhm \ Todo \ TaskCollection 10 ✔ är initialiserbar 15 ✔ lägger till en uppgift i samlingen 24 ✔ är talbar 29 ✔ räknar element i samlingen Petersuhm \ Todo \ Uppgift 10 ✔ är initialiserbar Petersuhm \ Todo \ TodoList 11 ✔ är initialiserbar 16 ✔ lägger till en uppgift till listan 24 ✔ kontrollerar om det har några uppgifter 3 specifikationer 8 exempel (8 passerade) 16msVi har effektivt beskrivit och uppnått det önskade beteendet hos vår kod. För att inte tala om, vår kod är 100% täckt av våra specifikationer, vilket innebär att refactoring inte kommer att vara en rädsla-inducerande upplevelse.
Genom att följa med, hoppas jag att du blev inspirerad för att ge phpspec ett försök. Det är mer än ett testverktyg - det är ett designverktyg. När du väl blivit van vid att använda phpspec (och dess fantastiska kodgenereringsverktyg), har du svårt att släppa igen det igen! Folk klagar ofta på att TDD eller BDD saktar ner dem. Efter att ha integrerat phpspec i mitt arbetsflöde känner jag verkligen motsatt sätt - min produktivitet förbättras väsentligt. Och min kod är mer solid!