I den andra delen av serien heter Laravel, BDD och You börjar vi beskriva och bygga vår första funktion med Behat och PhpSpec. I den senaste artikeln fick vi allting och såg hur lätt vi kan interagera med Laravel i våra Behat scenarier.
Nyligen skapade skaparen av Behat, Konstantin Kudryashov (a.c. everzet), en riktigt bra artikel som heter Introducing Modeling by Exempel. Arbetsflödet vi ska använda när vi bygger vår funktion är mycket inspirerad av den som everzet presenterade.
Kortfattat kommer vi att använda samma .funktion
att designa både vår kärndomän och vårt användargränssnitt. Jag har ofta känt att jag hade mycket dubbelarbete i mina funktioner i mina acceptans / funktionella och integrationspaket. När jag läste everzets förslag om att använda samma funktion för flera sammanhang klickade det hela på mig och jag tror att det är vägen att gå.
I vårt fall kommer vi att ha vårt funktionella sammanhang, vilket för närvarande också kommer att fungera som vårt acceptanslag och vårt integrationssätt som täcker vår domän. Vi börjar med att bygga upp domänen och sedan lägga till användargränssnittet och ramsspecifika saker efteråt.
För att kunna använda "shared feature, multiple context" -metoden måste vi göra några refactorings av vår befintliga installation.
Först ska vi ta bort välkomstfunktionen som vi gjorde i första delen, eftersom vi inte behöver det verkligen och det följer inte riktigt den generiska stilen vi behöver för att kunna använda flera sammanhang.
$ git rm funktioner / funktionell / welcome.feature
För det andra kommer vi att ha våra funktioner i roden av funktioner
mapp, så vi kan fortsätta och ta bort väg
attribut från vår behat.yml
fil. Vi ska också byta namn på LaravelFeatureContext
till FunctionalFeatureContext
(kom ihåg att ändra klassnamnet också):
standard: sviter: funktionell: kontekster: [FunctionalFeatureContext]
Slutligen, för att bara rensa upp saker, tror jag att vi borde flytta alla Laravel-relaterade saker till sitt eget drag:
# features / bootstrap / LaravelTrait.php app) $ this-> refreshApplication (); / ** * Skapar programmet. * * @return \ Symfony \ Component \ HttpKernel \ HttpKernelInterface * / public function createApplication () $ unitTesting = true; $ testEnvironment = 'testning'; retur kräver __DIR __. '/ ... / ... /bootstrap/start.php';
I FunctionalFeatureContext
vi kan då använda egenskapen och ta bort saker som vi just flyttat:
/ ** * Behörighetsklass. * / klass FunctionalFeatureContext implementerar SnippetAcceptingContext använd LaravelTrait; / ** * Initialiserar kontext. * * Varje scenario får sitt eget kontextobjekt. * Du kan också skicka godtyckliga argument till kontextkonstruktören genom behat.yml. * / allmän funktion __construct ()
Egenskaper är ett utmärkt sätt att städa upp dina sammanhang.
Som framgår av del ett, kommer vi att bygga en liten applikation för tidspårning. Den första funktionen handlar om spårningstid och generering av ett tidsblad från de spårade posterna. Här är funktionen:
Funktion: Spårningstid För att spåra tid som spenderas på uppgifter Som anställd behöver jag hantera ett tidskriftsnummer med tidsangivelser. Scenario: Generera tidskrifter Med tanke på att jag har följande tidsposter | uppgift | duration | | kodning | 90 | | kodning | 30 | | dokumentera | 150 | När jag genererar tidtabellen så ska min totala tid på kodning vara 120 minuter och min totala tid på dokumentation ska vara 150 minuter och min totala tid på möten ska vara 0 minuter och min totala tid ska vara 270 minuter
Kom ihåg att detta bara är ett exempel. Jag finner det lättare att definiera funktioner i det verkliga livet, eftersom du har ett verkligt problem du behöver lösa och får ofta chansen att diskutera funktionen med kollegor, kunder eller andra intressenter.
Okej, låt oss få Behat generera scenariot steg för oss:
$ leverantör / bin / behat --dry-run --append-snippets
Vi behöver justera de genererade stegen bara en liten bit. Vi behöver bara fyra steg för att täcka scenariot. Slutresultatet ska se ut så här:
/ ** * @Given Jag har följande tidsposter * / allmän funktion iHaveTheFollowingTimeEntries (TableNode $ tabell) släng ny PendingException (); / ** * @ När jag genererar tidskriften * / offentlig funktion iGenerateTheTimeSheet () kasta ny PendingException (); / ** * @Then min totala tid på: uppgift ska vara: expectedDuration minuter * / allmän funktion myTotalTimeSpentOnTaskShouldBeMinutes ($ uppgift, $ expectedDuration) släng ny PendingException (); / ** * @Then min totala tid ska vara: expectedDuration minuter * / allmän funktion myTotalTimeSpentShouldBeMinutes ($ expectedDuration) kasta ny PendingException ();
Vårt funktionella sammanhang är redo att gå nu, men vi behöver också ett sammanhang för vår integrationspaket. Först lägger vi till sviten till behat.yml
fil:
default: sviter: funktionell: kontext: [FunctionalFeatureContext] integration: kontekster: [IntegrationFeatureContext]
Därefter kan vi bara kopiera standard FeatureContext
:
$ cp funktioner / bootstrap / FeatureContext.php funktioner / bootstrap / IntegrationFeatureContext.php
Kom ihåg att byta klassnamn till IntegrationFeatureContext
och också att kopiera användningsansvaret för PendingException
.
Slutligen, eftersom vi delar funktionen kan vi bara kopiera de fyra stegdefinitionerna från det funktionella sammanhanget. Om du kör Behat ser du att funktionen körs två gånger: en gång för varje sammanhang.
Vid denna tidpunkt är vi beredda att börja fylla i de pågående stegen i vårt integrationskontext för att utforma kärndomänen i vår ansökan. Det första steget är Med tanke på att jag har följande tidsposter
, följt av ett bord med tidsregistreringsregister. Håll det enkelt, låt oss bara slingra över raderna på bordet, försök att ordna en tidsangivelse för var och en och lägga till dem i en postermatris på sammanhanget:
använd TimeTracker \ TimeEntry; ... / ** * @Given Jag har följande tidsposter * / allmän funktion iHaveTheFollowingTimeEntries (TableNode $ tabell) $ this-> entries = []; $ rader = $ tabell-> getHash (); foreach ($ rader som $ rad) $ entry = new TimeEntry; $ entry-> task = $ row ['task']; $ entry-> duration = $ row ['duration']; $ this-> poster [] = $ entry;
Running Behat kommer att orsaka ett dödligt fel, eftersom Timetracker \ TimeEntry
klass finns ännu inte. Det är här PhpSpec går in i scenen. I slutet, TimeEntry
kommer att bli en Eloquent-klass, även om vi inte oroar oss för det ännu. PhpSpec och ORMs som Eloquent spelar inte tillsammans så bra, men vi kan fortfarande använda PhpSpec för att generera klassen och till och med speciera lite grundläggande beteende. Låt oss använda PhpSpec generatorer för att generera TimeEntry
klass:
$ vendor / bin / phpspec desc "TimeTracker \ TimeEntry" $ säljare / bin / phpspec run Vill du att jag ska skapa TimeTracker \ TimeEntry för dig? y
När klassen är genererad behöver vi uppdatera autoloadavsnittet i vår composer.json
fil:
"autoload": "klasskarta": ["app / kommandon", "app / kontroller", "app / modeller", "app / databas / migreringar", "app / databas / frön"], "psr-4" : "TimeTracker \\": "src / TimeTracker",
Och självklart kör kompositör dump-autoload
.
Running PhpSpec ger oss grönt. Running Behat ger oss också grön. Vilken bra start!
Att låta Behat vägleda oss, hur är det med att vi bara går vidare till nästa steg, När jag genererar tidskriften
, direkt?
Nyckelordet här är "generera", vilket ser ut som en term från vår domän. I en programmerares värld kan översättningen "generera tidtabellen" för att koda bara betyda instantiating a Tidrapport
klass med en massa tidsposter. Det är viktigt att försöka hålla sig till språket från domänen när vi utformar vår kod. På så sätt hjälper vår kod till att beskriva vårt avsedda beteende.
Jag identifierar termen generera
lika viktigt för domänen, varför jag tycker att vi borde ha en statisk genereringsmetod på en Tidrapport
klass som fungerar som ett alias för konstruktören. Denna metod bör ta en samling tidsåtgångar och lagra dem på tidskriften.
Istället för att bara använda en array tror jag att det är vettigt att använda Illuminate \ Support \ Collection
klass som kommer med Laravel. Eftersom TimeEntry
kommer att vara en Eloquent-modell, när vi frågar databasen för tidsposter kommer vi ändå att få en av dessa Laravel-samlingar. Vad sägs om något så här:
använd Illuminate \ Support \ Collection; använd TimeTracker \ TimeSheet; använd TimeTracker \ TimeEntry; ... / ** * @ När jag genererar tidskriften * / public function iGenerateTheTimeSheet () $ this-> sheet = TimeSheet :: generera (Collection :: make ($ this-> entries));
Förresten kommer TimeSheet inte att bli en Eloquent-klass. Åtminstone för nu behöver vi bara göra tidsangivelserna kvar, och då kommer tidskrifterna bara att vara genererad från posterna.
Running Behat kommer återigen att orsaka ett dödligt fel, för Tidrapport
existerar inte. PhpSpec kan hjälpa oss att lösa det:
$ vendor / bin / phpspec desc "TimeTracker \ TimeSheet" $ säljare / bin / phpspec run Vill du att jag ska skapa TimeTracker \ TimeSheet för dig? y $ -leverantör / bin / phpspec kör $ leverantör / bin / behat PHP Felaktigt fel: Ring till odefinierad metod TimeTracker \ TimeSheet :: generera ()
Vi får fortfarande ett dödligt fel efter att ha skapat klassen, eftersom den statiska generera()
Metoden finns fortfarande inte. Eftersom det här är en väldigt enkel statisk metod tror jag inte att det finns behov av en spec. Det är inget annat än en omslag för konstruktören:
poster = $ poster; generera statisk statisk funktion (samling $ poster) returnera nya statiska ($ poster);
Detta kommer att få Behat tillbaka till grönt, men PhpSpec suger nu på oss och säger: Argument 1 skickad till TimeTracker \ TimeSheet :: __ construct () måste vara en förekomst av Illuminate \ Support \ Collection, inget givet
. Vi kan lösa detta genom att skriva en enkel låta()
funktion som kommer att kallas före varje spec:
put (new TimeEntry); $ This-> beConstructedWith ($ poster); funktion it_is_initializable () $ this-> shouldHaveType ('TimeTracker \ TimeSheet');
Detta kommer att få oss tillbaka till grönt över hela linjen. Funktionen ser till att tidskriften alltid är konstruerad med en mock i kollektionsklassen.
Vi kan nu säkert gå vidare till Sedan min totala tid spenderas på ...
steg. Vi behöver en metod som tar ett uppgiftsnamn och returnerar den ackumulerade varaktigheten för alla poster med detta uppgiftsnamn. Direkt översatt från gurka till kod, kan detta vara något liknande totalTimeSpentOn ($ uppgift)
:
/ ** * @Then min totala tid spenderas på: uppgiften ska vara: expectedDuration minutes * / public function myTotalTimeSpentOnTaskShouldBeMinutes ($ uppgift, $ expectedDuration) $ actualDuration = $ this-> sheet-> totalTimeSpentOn ($ task); PHPUnit :: assertEquals ($ expectedDuration, $ actualDuration);
Metoden existerar inte, så körs Behat ger oss Ring till odefinierad metod TimeTracker \ TimeSheet :: totalTimeSpentOn ()
.
För att specificera metoden ska vi skriva en spec som på något sätt liknar vad vi redan har i vårt scenario:
funktion 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]); $ This-> beConstructedWith ($ samling); $ This-> totalTimeSpentOn (sovande) -> shouldbe (240); $ This-> totalTimeSpentOn (äta) -> shouldbe (60);
Observera att vi inte använder mocks för TimeEntry
och Samling
instanser. Det här är vår integrationspaket och jag tror inte att det är nödvändigt att mocka ut det här. Objekten är ganska enkla och vi vill se till att objekten i vår domän interagerar som vi förväntar oss att de ska. Det finns förmodligen många åsikter om detta, men det här är meningsfullt för mig.
Flytta längs:
$ vendor / bin / phpspec run Vill du att jag ska skapa "TimeTracker \ TimeSheet :: totalTimeSpentOn ()" för dig? y $ leverantör / bin / phpspec köra 25 ✘ det ska beräkna total tid som spenderas på uppgift förväntad [heltal: 240], men blev null.
För att filtrera posterna kan vi använda filtrera()
metod på Samling
klass. En enkel lösning som får oss till grön:
offentlig funktion totalTimeSpentOn ($ uppgift) $ entries = $ this-> entries-> filter (funktion ($ entry) användning ($ uppgift) return $ entry-> task === $ task;); $ duration = 0; foreach ($ poster som $ entry) $ duration + = $ entry-> duration; returnera $ duration
Vår spec är grön, men jag känner att vi kan dra nytta av lite refactoring här. Metoden verkar göra två olika saker: filtrera poster och ackumulera varaktigheten. Låt oss extrahera den senare till sin egen metod:
offentlig funktion totalTimeSpentOn ($ uppgift) $ entries = $ this-> entries-> filter (funktion ($ entry) användning ($ uppgift) return $ entry-> task === $ task;); returnera $ this-> sumDuration ($ entries); skyddad funktion sumDuration ($ entries) $ duration = 0; foreach ($ poster som $ entry) $ duration + = $ entry-> duration; returnera $ duration
PhpSpec är fortfarande grönt och vi har nu tre gröna steg i Behat. Det sista steget ska vara enkelt att genomföra, eftersom det liknar det som vi just gjorde.
/ ** * @Then min totala tid ska vara: expectedDuration minuter * / allmän funktion myTotalTimeSpentShouldBeMinutes ($ expectedDuration) $ actualDuration = $ this-> sheet-> totalTimeSpent (); PHPUnit :: assertEquals ($ expectedDuration, $ actualDuration);
Running Behat ger oss Ring till odefinierad metod TimeTracker \ TimeSheet :: totalTimeSpent ()
. I stället för att göra ett separat exempel i vår spec för den här metoden, vad sägs om att vi bara lägger till den som vi redan har? Det kanske inte är helt i linje med vad som är "rätt" att göra, men låt oss vara lite pragmatiska:
... $ this-> beConstructedWith ($ collection); $ This-> totalTimeSpentOn (sovande) -> shouldbe (240); $ This-> totalTimeSpentOn (äta) -> shouldbe (60); $ This-> totalTimeSpent () -> shouldbe (300);
Låt PhpSpec generera metoden:
$ vendor / bin / phpspec run Vill du att jag ska skapa 'TimeTracker \ TimeSheet :: totalTimeSpent ()' för dig? y $ leverantör / bin / phpspec köra 25 ✘ det ska beräkna total tid som spenderas på uppgift förväntad [heltal: 300], men blev null.
Att komma till grönt är enkelt nu när vi har sumDuration ()
metod:
allmän funktion totalTimeSpent () return $ this-> sumDuration ($ this-> entries);
Och nu har vi en grön funktion. Vår domän utvecklas långsamt!
Nu flyttar vi till vår funktionella svit. Vi kommer att utforma användargränssnittet och hantera alla Laravel-specifika saker som inte är oro för vår domän.
När vi arbetar i den funktionella sviten kan vi lägga till -s
flagga för att instruera Behat att bara köra våra funktioner via FunctionalFeatureContext
:
$ leverantör / bin / behat -s funktionell
Det första steget kommer att likna det första integrationssammanhanget. I stället för att bara göra inmatningarna kvarstår i sammanhanget i en array, måste vi faktiskt få dem att kvarstå i en databas så att de kan hämtas senare:
använd TimeTracker \ TimeEntry; ... / ** * @Given Jag har följande tidsposter * / allmän funktion iHaveTheFollowingTimeEntries (TableNode $ tabell) $ rader = $ table-> getHash (); foreach ($ rader som $ rad) $ entry = new TimeEntry; $ entry-> task = $ row ['task']; $ entry-> duration = $ row ['duration']; $ Entry> Spara ();
Running Behat kommer att ge oss dödligt fel Ring till odefinierad metod TimeTracker \ TimeEntry :: save ()
, eftersom TimeEntry
fortfarande är inte en Eloquent-modell. Det är lätt att fixa:
namespace TimeTracker; klassen TimeEntry utökar \ Eloquent
Om vi kör Behat igen kommer Laravel att klaga på att den inte kan ansluta till databasen. Vi kan fixa detta genom att lägga till en database.php
fil till app / config / testning
katalog, för att lägga till anslutningsinformationen för vår databas. För större projekt vill du förmodligen använda samma databasserver för dina test och din produktionskodsbas, men i vårt fall använder vi bara en SQLite-databas i minnet. Det här är super enkelt att sätta upp med Laravel:
'sqlite', 'connections' => array ('sqlite' => array ('drivrutin' => 'sqlite', 'database' => ': minne:', 'prefix' => ;
Nu om vi kör Behat, kommer det att berätta för oss att det inte finns något time_entries
tabell. För att åtgärda detta måste vi göra en migrering:
$ php artisan migrera: skapa createTimeEntriesTable --create = "time_entries"
Schema :: skapa ('time_entries', funktion (Blueprint $ tabell) $ tabell-> steg ('id'); $ tabell-> sträng ('uppgift'); $ tabell-> heltal ('duration'); tabell-> tidsstämplar (););
Vi är fortfarande inte gröna, eftersom vi behöver ett sätt att instruera Behat att köra våra migreringar före varje scenario, så vi har en ren skiffer varje gång. Genom att använda Behats anteckningar kan vi lägga till dessa två metoder till LaravelTrait
drag:
/ ** * @BeforeScenario * / public function setupDatabase () $ this-> app ['artisan'] -> call ('migrera'); / ** * @AfterScenario * / allmän funktion cleanDatabase () $ this-> app ['artisan'] -> call ('migrera: återställ');
Detta är ganska snyggt och får vårt första steg till grönt.
Nästa upp är När jag genererar tidskriften
steg. Hur jag ser det, genererar tidskriften motsvarar att besöka index
Åtgärd av tidpunktsresursen, eftersom tidskriften är samlingen av alla tidsposter. Så tidslagsobjektet är som en behållare för alla tidsposter och ger oss ett bra sätt att hantera poster. I stället för att gå till / tidsanteckningar
, För att se tidskriften anser jag att arbetstagaren ska gå till / Tid-ark
. Vi bör säga det i vår steg definition:
/ ** * @ När jag genererar tidskriften * / offentlig funktion iGenerateTheTimeSheet () $ this-> call ('GET', '/ time-sheet'); $ this-> crawler = ny sökrobot ($ this-> client-> getResponse () -> getContent (), url ('/'));
Detta kommer att orsaka a NotFoundHttpException
, eftersom rutten ännu inte är definierad. Som jag bara förklarade, tycker jag att den här webbadressen ska överföras till index
Åtgärd på tidpunktsresursen:
Rutt :: get ('tid-ark', ['som' => 'time_sheet', 'uses' => 'TimeEntriesController @ index']);
För att komma till grönt måste vi generera regulatorn:
$ php artisan controller: gör TimeEntriesController $ composer dump-autoload
Och där går vi.
Slutligen måste vi krypa på sidan för att hitta den totala varaktigheten av tidsposter. Jag tror att vi kommer ha en sorts tabell som sammanfattar varaktigheten. De sista två stegen är så lika att vi bara ska genomföra dem samtidigt:
/ ** * @Then min totala tid spenderas på: uppgiften ska vara: expectedDuration minuter * / allmän funktion myTotalTimeSpentOnTaskShouldBeMinutes ($ uppgift, $ expectedDuration) $ actualDuration = $ this-> sökrobot-> filter ('td #'. $ Uppgift . 'TotalDuration') -> text (); PHPUnit :: assertEquals ($ expectedDuration, $ actualDuration); / ** * @Then min totala tid bör vara: expectedDuration minuter * / allmän funktion myTotalTimeSpentShouldBeMinutes ($ expectedDuration) $ actualDuration = $ this-> sökrobot-> filter ('td # totalDuration') -> text (); PHPUnit :: assertEquals ($ expectedDuration, $ actualDuration);
Crawleren letar efter a Eftersom vi fortfarande inte har en uppfattning, berättar sökroboten oss För att fixa detta, låt oss bygga Vyn, för nu, kommer bara att bestå av ett enkelt bord med sammanfattade varaktighetsvärden: Om du kör Behat igen ser du att vi framgångsrikt har implementerat funktionen. Kanske borde vi ta en stund att inse att inte ens en gång öppnade vi en webbläsare! Detta är en enorm förbättring av vårt arbetsflöde, och som en bra bonus har vi nu automatiska tester för vår ansökan. Jippie! Om du kör Om du kör dessa två kommandon: Du kommer att se att vi är tillbaka till grön, både med Behat och PhpSpec. Jippie! Vi har nu beskrivit och konstruerat vår första funktion, helt genom att använda en BDD-metod. Vi har sett hur vi kan dra nytta av att utforma kärndomänen i vår ansökan innan vi oroar oss för användargränssnittet och de ramspecifika sakerna. Vi har också sett hur lätt det är att interagera med Laravel, och i synnerhet databasen, i våra Behat-kontext. I nästa artikel kommer vi att göra en hel del refactoring för att undvika för mycket logik på våra Eloquent-modeller, eftersom dessa är svårare att testa i isolering och är tätt kopplade till Laravel. Håll dig igång! nod med ett ID på [Uppgiftsnamn] TotalDuration
eller Total varaktighet
i det sista exemplet.Den aktuella nodlistan är tom.
index
verkan. Först hämtar vi samlingen av tidsposter. För det andra genererar vi ett tidsblad från posterna och skickar det vidare till (fortfarande inte befintlig) vy.använd TimeTracker \ TimeSheet; använd TimeTracker \ TimeEntry; klass TimeEntriesController utökar \ BaseController / ** * Visa en lista över resursen. * * @return Response * / public function index () $ entries = TimeEntry :: alla (); $ sheet = TimeSheet :: generera ($ poster); returnera Visa :: make ('time_entries.index', compact ('sheet')); ...
Tidsschema
Uppgift Total varaktighet kodning $ ark-> totalTimeSpentOn ('kodning') dokumentera $ sheet-> totalTimeSpentOn ('dokumentering') möten $ sheet-> totalTimeSpentOn ('meetings') Total $ sheet-> totalTimeSpent () Slutsats
försäljaren / bin / behat
För att kunna köra båda Behat-sviterna ser du att båda är gröna nu. Om du kör PhpSpec men tyvärr ser du att våra specifikationer är trasiga. Vi får ett dödligt fel Klass "Eloquent" hittades inte i ...
. Detta beror på att Eloquent är ett alias. Om du tittar in app / config / app.php
under alias kommer du att se det Vältalig
är faktiskt ett alias för Illuminate \ Database \ Eloquent \ Model
. För att få PhpSpec tillbaka till grön, måste vi importera den här klassen:namespace TimeTracker; använd Illuminate \ Database \ Eloquent \ Model som Eloquent; klass TimeEntry utökar Eloquent
$ leverantör / bin / phpspec run; försäljaren / bin / behat