Laravel, BDD og deg Den første funksjonen

I den andre delen av denne serien, kalt Laravel, BDD og You, begynner vi å beskrive og bygge vår første funksjon ved hjelp av Behat og PhpSpec. I den siste artikkelen fikk vi alt satt opp og så hvor lett vi kan samhandle med Laravel i våre Behat scenarier.

Nylig skapte skaperen av Behat, Konstantin Kudryashov (a.c. everzet), en virkelig flott artikkel som heter Introducing Modeling by Example. Arbeidsflyten vi skal bruke, når vi bygger vår funksjon, er svært inspirert av den som Everzet presenterte. 

Kort sagt, vi skal bruke det samme .trekk å designe både vårt kjerneområde og vårt brukergrensesnitt. Jeg har ofte følt at jeg hadde mye duplisering i mine funksjoner i mine aksept / funksjonelle og integrering suiter. Når jeg leser everzets forslag om bruk av samme funksjon for flere sammenhenger, klarte det alle for meg, og jeg tror det er veien å gå. 

I vårt tilfelle vil vi ha vår funksjonelle kontekst, som for øyeblikket også vil fungere som vårt akseptasjonslag, og integrasjonskonteksten vår, som vil dekke vårt domene. Vi starter med å bygge domenet og deretter legge til brukergrensesnittet og rammespesifikke ting etterpå.

Små refactorings

For å kunne bruke "felles funksjon, flere sammenhenger" tilnærming, må vi gjøre noen refactorings av vårt eksisterende oppsett.

Først skal vi slette velkomstfunksjonen som vi gjorde i første del, siden vi ikke virkelig trenger det, og det følger ikke veldig med den generiske stilen vi trenger for å kunne bruke flere sammenhenger.

$ git rm funksjoner / funksjonell / welcome.feature

For det andre skal vi ha våre funksjoner i roten til egenskaper mappe, så vi kan gå videre og fjerne sti attributt fra vår behat.yml fil. Vi skal også omdøpe LaravelFeatureContext til FunctionalFeatureContext (Husk å endre klassenavnet også):

standard: suiter: funksjonell: sammenhenger: [FunctionalFeatureContext]

Til slutt, for å rydde opp ting litt, tror jeg at vi skal flytte alle Laravel-relaterte ting til sitt eget trekk:

# features / bootstrap / LaravelTrait.php app) $ this-> refreshApplication ();  / ** * Oppretter applikasjonen. * * @return \ Symfony \ Component \ HttpKernel \ HttpKernelInterface * / offentlig funksjon createApplication () $ unitTesting = true; $ testEnvironment = 'testing'; retur krever __DIR __. '/ ... / ... /bootstrap/start.php';  

FunctionalFeatureContext Vi kan da bruke egenskapen og slette de tingene vi nettopp har flyttet:

/ ** * Denne kontekstklasse. * / klasse FunctionalFeatureContext implementerer SnippetAcceptingContext use LaravelTrait; / ** * Initialiserer kontekst. * * Hvert scenario får sin egen kontekstobjekt. * Du kan også sende vilkårlige argumenter til kontekstkonstruktøren gjennom behat.yml. * / offentlig funksjon __construct () 

Egenskaper er en fin måte å rydde opp sammen med.

Dele en funksjon

Som vist i del ett, skal vi bygge en liten applikasjon for tidssporing. Den første funksjonen handler om å spore tid og generere et tidsark fra de spore oppføringene. Her er funksjonen:

Funksjon: Sporingstid For å spore tid brukt på oppgaver Som ansatt trenger jeg å administrere et tidsskrift med tidsoppføringer Scenario: Generering av tidsskrift Gitt Jeg har følgende tidspunkter | oppgave | varighet | | koding | 90 | | koding | 30 | | dokumentere | 150 | Når jeg genererer tidsskriftet, bør min totale tid brukt på koding være 120 minutter og min totale tid på dokumentasjon skal være 150 minutter og min totale tid på møter skal være 0 minutter og min totale tid skal være 270 minutter 

Husk at dette bare er et eksempel. Jeg finner det lettere å definere funksjoner i virkeligheten, siden du har et faktisk problem du må løse og ofte får muligheten til å diskutere funksjonen med kollegaer, klienter eller andre interessenter.

Ok, la oss få Behat generere scenarietrinnene for oss:

$ leverandør / bin / behat - dry-run --append-snippets

Vi trenger å justere de genererte trinnene bare en liten bit. Vi trenger bare fire trinn for å dekke scenariet. Sluttresultatet skal se slik ut:

/ ** * @Given Jeg har følgende tidsposter * / offentlig funksjon iHaveTheFollowingTimeEntries (TableNode $ table) kaste nye PendingException ();  / ** * @ Når jeg genererer tidsskriftet * / offentlig funksjon iGenerateTheTimeSheet () kaste nye PendingException ();  / ** * @Then min totale tid brukt på: oppgaven skal være: expectedDuration minutter * / offentlig funksjon myTotalTimeSpentOnTaskShouldBeMinutes ($ oppgave, $ expectedDuration) kaste nye PendingException ();  / ** * @Then min totale tid bør være: expectedDuration minutter * / offentlig funksjon myTotalTimeSpentShouldBeMinutes ($ expectedDuration) kaste nye PendingException ();  

Vår funksjonelle kontekst er klar til å gå nå, men vi trenger også en kontekst for integrasjonspakken. Først vil vi legge til suiten til behat.yml fil:

standard: suiter: funksjonelle: sammenhenger: [FunctionalFeatureContext] integrasjon: kontekster: [IntegrationFeatureContext] 

Deretter kan vi bare kopiere standard FeatureContext:

$ cp funksjoner / bootstrap / FeatureContext.php funksjoner / bootstrap / IntegrationFeatureContext.php 

Husk å endre klassenavnet til IntegrationFeatureContext og også å kopiere brukserklæringen for PendingException.

Til slutt, siden vi deler funksjonen, kan vi bare kopiere de fire trinnsdefinisjonene fra den funksjonelle konteksten. Hvis du kjører Behat, vil du se at funksjonen kjøres to ganger: en gang for hver kontekst.

Designe domenet

På dette tidspunktet er vi klare til å begynne å fylle ut de ventende trinnene i integrasjonskonteksten vår for å designe kjernedomenet i søknaden vår. Det første trinnet er Gitt Jeg har følgende innspillinger, etterfulgt av et bord med tidsregistreringsrekord. For å holde det enkelt, la oss bare gå over tabellens rader, prøv å ordne en tidsoppføring for hver av dem, og legg dem til et oppføringsarrangement på konteksten:

bruk TimeTracker \ TimeEntry; ... / ** * @Given Jeg har følgende tidspunkter * / offentlig funksjon iHaveTheFollowingTimeEntries (TableNode $ tabell) $ this-> entries = []; $ rader = $ tabell-> getHash (); foreach ($ rader som $ rad) $ entry = new TimeEntry; $ entry-> task = $ row ['oppgave']; $ entry-> duration = $ row ['duration']; $ this-> oppføringer [] = $ entry;  

Running Behat vil føre til en dødelig feil, siden Timetracker \ TimeEntry klassen eksisterer ikke ennå. Det er her PhpSpec går inn i scenen. Til slutt, TimeEntry kommer til å bli en eloquent-klasse, selv om vi ikke bekymrer oss om det ennå. PhpSpec og ORMs som Eloquent spiller ikke sammen så bra, men vi kan fortsatt bruke PhpSpec til å generere klassen og til og med utpeke noen grunnleggende oppførsel. La oss bruke PhpSpec generatorer til å generere TimeEntry klasse:

$ vendor / bin / phpspec desc "TimeTracker \ TimeEntry" $ leverandør / bin / phpspec løp Vil du at jeg skal lage TimeTracker \ TimeEntry for deg? y 

Etter at klassen er generert, må vi oppdatere autoload-delen av vår composer.json fil:

"autoload": "klassekart": ["app / kommandoer", "app / kontroller", "app / modeller", "app / database / migrasjoner", "app / database / frø"], "psr-4" : "TimeTracker \\": "src / TimeTracker", 

Og selvfølgelig løp komponist dump-autoload.

Kjører PhpSpec gir oss grønt. Kjører Behat gir oss også grønn. For en flott start!

La Behat lede vår vei, hva med hvordan vi bare går videre til neste trinn, Når jeg genererer tidsskriftet, med en gang?

Søkeordet her er "generere", som ser ut som et begrep fra domenet vårt. I en programmererverden kan oversette "generere tidsskjemaet" til kode bare bety å instansere a Tids skjema klasse med en mengde tidsposter. Det er viktig å prøve å holde seg til språket fra domenet når vi designer vår kode. På den måten vil vår kode bidra til å beskrive den påtenkte oppførselen til søknaden vår. 

Jeg identifiserer begrepet generere så viktig for domenet, og derfor tror jeg at vi burde ha en statisk genereringsmetode på en Tids skjema klasse som tjener som et alias for konstruktøren. Denne metoden bør ta en samling av tidsoppføringer og lagre dem på tidsarket. 

I stedet for bare å bruke en matrise, tror jeg det vil være fornuftig å bruke Belyse \ Support \ Collection klasse som følger med Laravel. Siden TimeEntry vil være en Eloquent-modell, når vi spørre databasen for tidsoppføringer, vil vi likevel få en av disse Laravel-samlingene. Hva med noe som dette:

bruk Illuminate \ Support \ Collection; bruk TimeTracker \ TimeSheet; bruk TimeTracker \ TimeEntry; ... / ** * @ Når jeg genererer tidsskriftet * / offentlig funksjon iGenerateTheTimeSheet () $ this-> sheet = TimeSheet :: generere (Samling :: lage ($ dette-> oppføringer));  

Forresten, vil TimeSheet ikke være en Eloquent-klasse. I hvert fall for nå trenger vi bare å gjøre tidspostene vedvarende, og så vil tidsarkene bare være generert fra oppføringene.

Running Behat vil igjen forårsake en dødelig feil, fordi Tids skjema eksisterer ikke. PhpSpec kan hjelpe oss med å løse det:

$ vendor / bin / phpspec desc "TimeTracker \ TimeSheet" $ leverandør / bin / phpspec løpe Vil du at jeg skal lage TimeTracker \ TimeSheet for deg? y $ leverandør / bin / phpspec kjøre $ leverandør / bin / behat PHP Fatal feil: Ring til udefinert metode TimeTracker \ TimeSheet :: generere () 

Vi får fortsatt en dødelig feil etter å ha opprettet klassen, fordi den statiske generere() Metoden eksisterer fortsatt ikke. Siden dette er en veldig enkel statisk metode, tror jeg ikke det er behov for en spesifikasjon. Det er ikke noe mer enn en wrapper for konstruktøren:

oppføringer = $ oppføringer;  generere statisk statisk funksjon (Samling $ oppføringer) returner ny statisk ($ oppføringer);  

Dette vil få Behat tilbake til grønt, men PhpSpec suger nå på oss og sier: Argument 1 passert til TimeTracker \ TimeSheet :: __ construct () må være en forekomst av Illuminate \ Support \ Collection, ingen gitt. Vi kan løse dette ved å skrive en enkel la() funksjon som vil bli kalt før hver spesifikasjon:

sett (ny TimeEntry); $ Dette-> beConstructedWith ($ oppføringer);  funksjon it_is_initializable () $ this-> shouldHaveType ('TimeTracker \ TimeSheet');  

Dette vil få oss tilbake til grønt over hele linjen. Funksjonen sørger for at tidsarket alltid er konstruert med en mock av samlingsklassen.

Vi kan nå bevege oss videre til Så min tid brukt på ...  skritt. Vi trenger en metode som tar et oppgavenavn og returnerer akkumulert varighet for alle oppføringer med dette oppgavenavnet. Direkte oversatt fra kork til kode, kan dette være noe sånt totalTimeSpentOn ($ oppgave):

/ ** * @Then min totale tid brukt på: oppgaven skal være: expectedDuration minutter * / offentlig funksjon myTotalTimeSpentOnTaskShouldBeMinutes ($ oppgave, $ expectedDuration) $ actualDuration = $ this-> sheet-> totalTimeSpentOn ($ oppgave); PHPUnit :: assertEquals ($ expectedDuration, $ actualDuration);  

Metoden eksisterer ikke, så kjører Behat vil gi oss Ring til udefinert metode TimeTracker \ TimeSheet :: totalTimeSpentOn ().

For å spesifisere metoden, vil vi skrive en spesifikasjon som på en eller annen måte ser ut som det vi allerede har i scenariet vårt:

funksjon it_should_calculate_total_time_spent_on_task () $ entry1 = new TimeEntry; $ entry1-> task = 'sleeping'; $ entry1-> duration = 120; $ entry2 = new TimeEntry; $ entry2-> task = 'eating'; $ entry2-> duration = 60; $ entry3 = new TimeEntry; $ entry3-> task = 'sleeping'; $ entry3-> duration = 120; $ collection = Collection :: make ([$ entry1, $ entry2, $ entry3]); $ Dette-> beConstructedWith ($ samling); $ Dette-> totalTimeSpentOn ( 'sovende') -> shouldbe (240); $ Dette-> totalTimeSpentOn ( 'spise') -> shouldbe (60);  

Vær oppmerksom på at vi ikke bruker mocks for TimeEntry og Samling forekomster. Dette er vår integrering suite, og jeg tror ikke det er behov for å mocke dette ut. Objektene er ganske enkle, og vi vil sørge for at objektene i vårt domene samhandler som vi forventer at de skal. Det er nok mange meninger om dette, men dette gir mening for meg.

Flytte sammen:

$ leverandør / bin / phpspec-løp Vil du at jeg skal opprette 'TimeTracker \ TimeSheet :: totalTimeSpentOn ()' for deg? y $ leverandør / bin / phpspec løpe 25 ✘ det skal beregne total tid brukt på oppgaven forventet [heltall: 240], men ble null. 

For å filtrere oppføringene, kan vi bruke filter() metode på Samling klasse. En enkel løsning som gir oss grønn:

offentlig funksjon totalTimeSpentOn ($ oppgave) $ entries = $ this-> entries-> filter (funksjon ($ entry) bruk ($ oppgave) return $ entry-> oppgave === $ oppgave;); $ varighet = 0; foreach ($ oppføringer som $ entry) $ duration + = $ entry-> varighet;  returner $ varighet;  

Vår spesifikasjon er grønn, men jeg føler at vi kunne dra nytte av noe refactoring her. Metoden ser ut til å gjøre to forskjellige ting: filteroppføringer og akkumulere varigheten. La oss trekke ut sistnevnte til egen metode:

offentlig funksjon totalTimeSpentOn ($ oppgave) $ entries = $ this-> entries-> filter (funksjon ($ entry) bruk ($ oppgave) return $ entry-> oppgave === $ oppgave;); returnere $ this-> sumDuration ($ entries);  beskyttet funksjon sumDuration ($ entries) $ duration = 0; foreach ($ oppføringer som $ entry) $ duration + = $ entry-> varighet;  returner $ varighet;  

PhpSpec er fortsatt grønn, og vi har nå tre grønne trinn i Behat. Det siste trinnet skal være enkelt å implementere, siden det er noe som ligner på det vi nettopp gjorde.

/ ** * @Then min totale tid bør være: expectedDuration minutter * / offentlig funksjon myTotalTimeSpentShouldBeMinutes ($ expectedDuration) $ actualDuration = $ this-> sheet-> totalTimeSpent (); PHPUnit :: assertEquals ($ expectedDuration, $ actualDuration);  

Running Behat vil gi oss Ring til udefinert metode TimeTracker \ TimeSheet :: totalTimeSpent (). I stedet for å gjøre et eget eksempel i vår spesifikasjon for denne metoden, hva skjer med å legge den til den vi allerede har? Det er kanskje ikke helt i tråd med hva som er "riktig" å gjøre, men la oss være litt pragmatiske:

... $ this-> beConstructedWith ($ samling); $ Dette-> totalTimeSpentOn ( 'sovende') -> shouldbe (240); $ Dette-> totalTimeSpentOn ( 'spise') -> shouldbe (60); $ Dette-> totalTimeSpent () -> shouldbe (300); 

La PhpSpec generere metoden:

$ vendor / bin / phpspec run Vil du at jeg skal lage TimeTracker \ TimeSheet :: totalTimeSpent () for deg? y $ leverandør / bin / phpspec kjøre 25 ✘ det skal beregne total tid brukt på oppgaven forventet [heltall: 300], men ble null. 

Å komme til grønt er lett nå som vi har sumDuration () metode:

offentlig funksjon totalTimeSpent () return $ this-> sumDuration ($ this-> entries);  

Og nå har vi en grønn funksjon. Vårt domene er langsomt utviklet!

Utforming av brukergrensesnittet

Nå flytter vi til vår funksjonelle suite. Vi skal designe brukergrensesnittet og håndtere alle Laravel-spesifikke ting som ikke er bekymringen for domenet vårt.

Mens vi jobber i den funksjonelle pakken, kan vi legge til -s flagg for å instruere Behat å bare kjøre våre funksjoner gjennom FunctionalFeatureContext:

$ leverandør / bin / behat -s funksjonell 

Det første trinnet skal se ut som det første av integrasjonskonteksten. I stedet for å bare gjøre oppføringene vedvarende på konteksten i en matrise, må vi faktisk gjøre dem vedvarende i en database slik at de kan hentes senere:

bruk TimeTracker \ TimeEntry; ... / ** * @Given Jeg har følgende tidspunkter * / offentlig funksjon iHaveTheFollowingTimeEntries (TableNode $ table) $ rader = $ table-> getHash (); foreach ($ rader som $ rad) $ entry = new TimeEntry; $ entry-> task = $ row ['oppgave']; $ entry-> duration = $ row ['duration']; $ Innløps-> Lagre ();  

Running Behat vil gi oss dødelig feil Ring til udefinert metode TimeTracker \ TimeEntry :: save (), siden TimeEntry Det er fortsatt ikke en Eloquent-modell. Det er lett å fikse:

namespace TimeTracker; klasse TimeEntry strekker seg \ Eloquent  

Hvis vi kjører Behat igjen, vil Laravel klage på at den ikke kan koble til databasen. Vi kan fikse dette ved å legge til en database.php fil til app / konfig / testing katalog, for å legge til tilkoblingsdetaljer for vår database. For større prosjekter vil du sannsynligvis bruke samme databaseserver for testene og produksjonskoden din, men i vårt tilfelle vil vi bare bruke en SQLite-database i minnet. Dette er super enkelt å sette opp med Laravel:

 'sqlite', 'connections' => array ('sqlite' => array ('driver' => 'sqlite', 'database' => ': minne:', 'prefix' => ") ; 

Nå hvis vi kjører Behat, vil det fortelle oss at det er nei time_entries bord. For å fikse dette må vi foreta en migrering:

$ php artisan migrere: lag createTimeEntriesTable --create = "time_entries" 
Schema :: create ('time_entries', funksjon (Blueprint $ tabell) $ table-> trinn ('id'); $ table-> string ('oppgave'); $ table-> heltall tabell-> tidsstempler ();); 

Vi er fortsatt ikke grønne, siden vi trenger en måte å instruere Behat på å drive våre migrasjoner før hvert scenario, så vi har en ren skifer hver gang. Ved å bruke Behats annotasjoner kan vi legge til disse to metodene til LaravelTrait trekk:

/ ** * @BeforeScenario * / offentlig funksjon setupDatabase () $ this-> app ['artisan'] -> call ('migrere');  / ** * @AfterScenario * / offentlig funksjon cleanDatabase () $ this-> app ['artisan'] -> call ('migrere: reset');  

Dette er ganske pent og blir vårt første skritt til grønt.

Neste opp er Når jeg genererer tidsskriftet skritt. Måten jeg ser på, genererer tidsskriftet er like å besøke index handling av tidsoppføringsressursen, siden tidsarket er samlingen av alle tidsoppføringene. Så tidsarkobjektet er som en beholder for alle tidspunkter og gir oss en fin måte å håndtere oppføringer på. I stedet for å gå til / time-poster, For å se tidsskriftet tror jeg at arbeideren skal gå til /tids skjema. Vi bør sette det i vår trinndefinisjon:

/ ** * @ Når jeg genererer tidsskriftet * / offentlig funksjon iGenerateTheTimeSheet () $ this-> call ('GET', '/ time-sheet'); $ this-> crawler = ny Crawler ($ this-> client-> getResponse () -> getContent (), url ('/'));  

Dette vil forårsake a NotFoundHttpException, siden ruten er ikke definert enda. Som jeg nettopp forklarte, tror jeg at denne nettadressen skal kartlegges til index handling på tidspunktet for inntasting av ressurs:

Rute :: få ('tidsskrift', ['som' => 'time_sheet', 'uses' => 'TimeEntriesController @ index']); 

For å komme til grønt må vi generere kontrolleren:

$ php artisan controller: gjør TimeEntriesController $ composer dump-autoload 

Og der går vi.

Til slutt må vi krysse siden for å finne den totale varigheten av tidsoppføringene. Jeg regner med at vi vil ha en slags bord som oppsummerer varigheten. De to siste trinnene er så like at vi bare skal implementere dem samtidig:

/ ** * @Then min totale tid brukt på: oppgaven skal være: expectedDuration minutter * / offentlig funksjon myTotalTimeSpentOnTaskShouldBeMinutes ($ oppgave, $ expectedDuration) $ actualDuration = $ this-> crawler-> filter ('td #'. $ Oppgave . 'TotalDuration') -> tekst (); PHPUnit :: assertEquals ($ expectedDuration, $ actualDuration);  / ** * @Then min totale tid bør være: forventetDurasjon minutter * / offentlig funksjon myTotalTimeSpentShouldBeMinutes ($ expectedDuration) $ actualDuration = $ this-> crawler-> filter ('td # totalDuration') -> tekst (); PHPUnit :: assertEquals ($ expectedDuration, $ actualDuration);  

Crawleren ser etter en  nod med et ID på [Task_name] TotalDuration eller totalDuration i det siste eksemplet.

Siden vi fortsatt ikke har en visning, vil robotsøkeprogrammet fortelle oss det Den nåværende nodelisten er tom.

For å fikse dette, la oss bygge index handling. Først henter vi samlingen av tidsoppføringer. For det andre genererer vi et tidsark fra oppføringene og sender det videre til (fortsatt ikke eksisterende) visningen.

bruk TimeTracker \ TimeSheet; bruk TimeTracker \ TimeEntry; klasse TimeEntriesController utvider \ BaseController / ** * Vis en liste over ressursen. * * @return Response * / public function index () $ entries = TimeEntry :: alle (); $ sheet = TimeSheet :: generere ($ entries); returvisning :: lag ('time_entries.index', kompakt ('ark'));  ... 

Utsikten, for nå, skal bare bestå av et enkelt bord med oppsummerte varighetsverdier:

Tids skjema

Oppgave Total varighet
koding $ sheet-> totalTimeSpentOn ('koding')
dokumentere $ sheet-> totalTimeSpentOn ('dokumentere')
møter $ sheet-> totalTimeSpentOn ('møter')
Total $ ark-> totalTimeSpent ()

Hvis du kjører Behat igjen, vil du se at vi har implementert funksjonen. Kanskje bør vi ta et øyeblikk til å innse at ikke en gang åpnet vi en nettleser! Dette er en enorm forbedring av arbeidsflyten vår, og som en fin bonus har vi nå automatiserte tester for søknaden vår. Jippi!

Konklusjon

Hvis du kjører leverandør / bin / behat For å kunne kjøre begge Behat-suitene, vil du se at begge er grønn nå. Hvis du kjører PhpSpec skjønt, vil du dessverre se at våre spesifikasjoner er ødelagte. Vi får en dødelig feil Klassen 'Eloquent' ikke funnet i ... . Dette er fordi Eloquent er et alias. Hvis du tar en titt inn app / config / app.php under aliaser vil du se det Veltalende er faktisk et alias for Belyse \ Database \ Veltalende \ Modell. For å få PhpSpec tilbake til grønt, må vi importere denne klassen:

namespace TimeTracker; bruk Illuminate \ Database \ Eloquent \ Model som Eloquent; klasse TimeEntry utvider Eloquent  

Hvis du kjører disse to kommandoene:

$ leverandør / bin / phpspec kjøre; leverandør / bin / behat 

Du vil se at vi er tilbake til grønt, både med Behat og PhpSpec. Jippi! 

Vi har nå beskrevet og designet vår første funksjon, helt ved hjelp av en BDD-tilnærming. Vi har sett hvordan vi kan dra nytte av å utforme kjernedomenet i søknaden vår, før vi bekymrer oss for brukergrensesnittet og rammespesifikke ting. Vi har også sett hvor enkelt det er å samhandle med Laravel, og spesielt databasen, i våre Behat-sammenhenger. 

I neste artikkel skal vi gjøre mye refactoring for å unngå for mye logikk på våre Eloquent-modeller, siden disse er vanskeligere å teste i isolasjon og er tett koblet til Laravel. Følg med!