Om du jämför PhpSpec med andra testramar kommer du att upptäcka att det är ett mycket sofistikerat och uppfattat verktyg. En orsak till detta är att PhpSpec inte är ett testramverk som de som du redan vet.
I stället är det ett designverktyg som hjälper till att beskriva beteendet hos programvaran. En bieffekt av att beskriva mjukvarans beteende med PhpSpec är att du kommer att sluta med specifikationer som också kommer att fungera som test efteråt.
I den här artikeln tar vi en titt under Hood of Hood och försöker få en djupare förståelse för hur det fungerar och hur man använder det.
Om du vill borsta upp på phpspec, ta en titt på min starta handledning.
Låt oss börja med att titta på några av de nyckelbegrepp och klasser som bildar PhpSpec.
$ detta
Förstå vad $ detta
refererar till är nyckeln till att förstå hur PhpSpec skiljer sig från andra verktyg. I grund och botten, $ detta
hänvisa till en instans av den aktuella klassen som testas. Låt oss försöka undersöka detta lite mer för att bättre förstå vad vi menar.
Först och främst behöver vi en spec och en klass att leka med. Som du vet gör PhpSpecs generatorer detta super enkelt för oss:
$ phpspec desc "Suhm \ HelloWorld" $ phpspec run Vill du att jag ska skapa 'Suhm \ HelloWorld' för dig? y
Nästa upp, öppna den genererade specfilen och låt oss försöka få lite mer information om $ detta
:
shouldHaveType ( 'Suhm \ Helloworld'); var_dump (get_class ($ detta));
get_class ()
returnerar klassnamnet på ett givet objekt. I det här fallet kastar vi bara $ detta
där för att se vad den återvänder:
$ string (24) "spec \ Suhm \ HelloWorldSpec"
Okej, så inte för förvånansvärt, get_class ()
berättar det för oss $ detta
är en förekomst av spec \ Suhm \ HelloWorldSpec
. Det här är vettigt eftersom det här är bara vanlig gammal PHP-kod. Om vi istället använde get_parent_class ()
, vi skulle fåPhpSpec \ ObjectBehavior
, eftersom vår spec utökar denna klass.
Kom ihåg att jag bara berättade för det $ detta
faktiskt hänvisat till klassen som testas, vilket skulle varaSuhm \ Helloworld
i vårat fall? Som du kan se, återgår värdet av get_class ($ detta)
står i motsats till $ This-> shouldHaveType (Suhm \ Helloworld ");
.
Låt oss prova något annat:
shouldHaveType ( 'Suhm \ Helloworld'); var_dump (get_class ($ detta)); $ This-> dumpThis () -> shouldReturn (spec \ Suhm \ HelloWorldSpec ');
Med ovanstående kod försöker vi kalla en metod som heter dumpThis ()
på Hej världen
exempel. Vi kedjar en förväntan på metodanropet, förväntar oss att funktionens returvärde är en sträng som innehåller"Spec \ Suhm \ HelloWorldSpec"
. Detta är returvärdet från get_class ()
på linjen ovan.
Igen kan PhpSpec-generatorer hjälpa oss med vissa ställningar:
$ phpspec run Vill du att jag ska skapa 'Suhm \ HelloWorld :: dumpThis ()' för dig? y
Låt oss försöka ringa get_class ()
inifrån dumpThis ()
för:
Återigen, inte överraskande, får vi:
10 ✘ det är initialiserbart förväntat "spec \ Suhm \ HelloWorldSpec", men fick "Suhm \ HelloWorld".Det verkar som om vi saknar något här. Jag började med att berätta det
$ detta
hänvisar inte till vad du tycker det gör, men hittills har våra experiment inte visat något oväntat. Bortsett från en sak: Hur kunde vi ringa$ This-> dumpThis ()
innan det existerade utan PHP squeaking på oss?För att förstå detta måste vi dyka in i PhpSpec-källkoden. Om du vill ta en titt själv kan du läsa koden på GitHub.
Ta en titt på följande kod från
src / PhpSpec / ObjectBehavior.php
(den klass som vår spec sträcker sig):/ ** * Proxies alla samtal till PhpSpec-ämnet * * @paramsträng $ metod * @param array $ arguments * * @return mixed * / public function __call ($ metod, array $ arguments = array ()) return call_user_func_array array ($ this-> object, $ method), $ arguments);Kommentarerna ger det mesta av det:
"Proxies alla ringer till PhpSpec-ämnet"
. PHP__ring upp
Metod är en magisk metod som kallas automatiskt när en metod inte är tillgänglig (eller ej existerande).Det innebär att när vi försökte ringa
$ This-> dumpThis ()
, Samtalet var tydligen proxied till PhpSpec-ämnet. Om du tittar på koden kan du se att metallsamtalet är proxied till$ This-> -objekt
. (Detsamma gäller för egenskaper i vår förekomst. De är alla proxied till ämnet, med andra magiska metoder. Ta en titt i källan för att se själv.)Låt oss samråda
get_class ()
en gång till och se vad den har att säga om$ This-> -objekt
:shouldHaveType ( 'Suhm \ Helloworld'); var_dump (get_class ($ this-> Objekt));Och se vad vi får:
sträng (23) "PhpSpec \ Wrapper \ Subject"Mer om
Ämne
Ämne
är en omslag och implementerarPhpSpec \ Wrapper \ WrapperInterface
. Det är en kärna del av PhpSpec och möjliggör för alla [till synes] magi som ramverket kan göra. Det sveper en instans av den klass vi testar, så att vi kan göra alla sorters saker som ringer och egenskaper som inte existerar och ställer förväntningar.Som nämnts är PhpSpec mycket uppfattad över hur du ska skriva och specificera din kod. En specifik karta till en klass. Du har bara ett ämne per specifikation, vilken PhpSpec kommer noggrant slingra för dig. Det viktiga att notera om detta är att det här låter dig använda
$ detta
som om det var den faktiska förekomsten och gör det möjligt för läsliga och meningsfulla specifikationer.PhpSpec innehåller a
Omslag
vilket tar hand om instantiating theÄmne
. Den packarÄmne
med det faktiska objektet vi specificerar. EftersomÄmne
implementerarWrapperInterface
det måste ha engetWrappedObject ()
metod som ger oss tillgång till objektet. Det här är objektet som vi letade efter tidigare medget_class ()
.Låt oss prova igen:
shouldHaveType ( 'Suhm \ Helloworld'); var_dump (get_class ($ this-> objekt> getWrappedObject ())); // Och bara för att vara helt säker: var_dump ($ this-> object-> getWrappedObject () -> dumpThis ());Och där går du:
$ vendor / bin / phpspec löpband (15) "Suhm \ HelloWorld" -strängen (15) "Suhm \ HelloWorld"Trots att många saker händer bakom scenen, arbetar vi till slut med själva objektet förekomsten av
Suhm \ Helloworld
. Allt är bra.Tidigare när vi ringde
$ This-> dumpThis ()
, vi lärde oss hur samtalet faktiskt var proxied tillÄmne
. Vi lärde oss också detÄmne
är bara en omslag och inte själva objektet.Med denna kunskap är det uppenbart att vi inte kan ringa
dumpThis ()
påÄmne
utan en annan magisk metod.Ämne
har en__ring upp()
metod också:/ ** * @paramsträng $ metod * @param array $ arguments * * @return mixed | Ämne * / allmän funktion __call ($ metod, array $ arguments = array ()) if (0 === strpos , "borde")) returnera $ this-> callExpectation ($ metod, $ argumenter); returnera $ this-> caller-> call ($ method, $ arguments);Denna metod gör en av två saker. Först kontrollerar den om metodenavnet börjar med "bör". Om det gör det är det en förväntan, och samtalet delegeras till en metod som heter
callExpectation ()
. Om inte, delegeras samtalet i stället till en instans avPhpSpec \ Wrapper \ Ämne \ Caller
.Vi kommer att ignorera
Uppringare
tills vidare. Den innehåller också det inslagna objektet och vet hur man ringer metoder på den. DeUppringare
returnerar en wrapped instans när det kallar metoder på ämnet, så att vi kan kedja förväntningar på metoder, som vi gjorde meddumpThis ()
.Låt oss ta en titt på
callExpectation ()
metod:/ ** * @paramsträng $ metod * @param array $ arguments * * @return mixed * / privat funktion callExpectation ($ metod, array $ arguments) $ subject = $ this-> makeSureWeHaveASubject (); $ expectation = $ this-> expectationFactory-> skapa ($ metod, $ ämne, $ argumenter); om (0 === strpos ($ metod, 'shouldNot')) return $ expectation-> match (lcfirst (substr ($ metod, 9)), $ this, $ arguments, $ this-> wrappedObject); returnera $ expectation-> match (lcfirst (substr ($ method, 6)), $ this, $ arguments, $ this-> wrappedObject);Denna metod är ansvarig för att bygga en instans av
PhpSpec \ Wrapper \ Ämne \ Förväntan \ ExpectationInterface
. Detta gränssnitt dikterar amatch()
metod, somcallExpectation ()
samtal för att kontrollera förväntan. Det finns fyra olika slags förväntningar:Positiv
,Negativ
,PositiveThrow
ochNegativeThrow
. Var och en av dessa förväntningar innehåller en förekomst avPhpSpec \ Matcher \ MatcherInterface
Att denmatch()
metodanvändning. Låt oss titta på matchare nästa.matchers
Matchare är vad vi använder för att bestämma beteendet hos våra objekt. När vi skriver
skall…
ellerborde inte…
, vi använder en matchare Du hittar en omfattande lista över PhpSpec-matchare på min personliga blogg.Det finns många matchare som ingår i PhpSpec, som alla utökar
PhpSpec \ Matcher \ BasicMatcher
klass, som implementerarMatcherInterface
. Hur matcharna fungerar är ganska rakt framåt. Låt oss ta en titt på det tillsammans och jag uppmanar dig att ta en titt på källkoden också.Låt oss exempelvis se den här koden från
IdentityMatcher
:/ ** * @var array * / privat statisk $ keywords = array ('return', 'be', 'equal', 'beEqualTo'); / ** * @paramsträng $ namn * @param blandat $ ämne * @param array $ arguments * * @return bool * / offentliga funktionstöd ($ namn, $ ämne, array $ arguments) return in_array ($ name, self :: $ sökord) && 1 == count ($ arguments);De
stöd ()
Metoden dikteras avMatcherInterface
. I det här fallet fyra alias definieras för matcharen i$ sökord
array. Detta gör det möjligt för matcharen att stödja antingen:shouldReturn ()
,borde vara()
,shouldEqual ()
ellershouldBeEqualTo ()
, ellershouldNotReturn ()
,bör inte vara()
,shouldNotEqual ()
ellershouldNotBeEqualTo ()
.Från
BasicMatcher
, två metoder ärva:positiveMatch ()
ochnegativeMatch ()
. De ser så här ut:/ ** * @paramsträng $ namn * @param blandat $ ämne * @param array $ arguments * * @return mixed * * @throws FailureException * / slutlig offentlig funktion positivMatch ($ name, $ subject, array $ arguments) if (false === $ this-> matchningar ($ subject, $ arguments)) kasta $ this-> getFailureException ($ name, $ subject, $ arguments); returnera $ subjectDe
positiveMatch ()
Metoden kastar ett undantag omtändstickor()
metod (abstrakt metod som matcharna måste genomföra) returnerarfalsk
. DenegativeMatch ()
Metoden fungerar motsatt sätt. Detändstickor()
metod förIdentityMatcher
använder===
operatör för att jämföra$ ämne
med argumentet som levereras till matchningsmetoden:/ ** * @param blandat $ ämne * @param array $ arguments * * @return bool * / skyddad funktion matcher ($ subject, array $ arguments) return $ subject === $ arguments [0];Vi kan använda matcharen så här:
$ This-> getUser () -> shouldNotBeEqualTo ($ anotherUser);Vilket skulle så småningom ringa
negativeMatch ()
och se till atttändstickor()
returnerar false.Titta på några av de andra matcharna och se vad de gör!
Löfte om mer magi
Innan vi avslutar den här korta rundan i PhpSpecs internaler, låt oss ta en titt på ytterligare en bit magi:
shouldHaveType ( 'Suhm \ Helloworld'); var_dump (get_class ($ objekt));Genom att lägga till den typ som antyds
$ object
parameter till vårt exempel, PhpSpec kommer automatiskt att använda reflektion för att injicera en instans av klassen för att vi ska kunna använda. Men med de saker vi såg redan, litar vi verkligen på att vi verkligen får en förekomst avStdClass
? Låt oss samrådaget_class ()
en gång till:$ vendor / bin / phpspec körsträng (28) "PhpSpec \ Wrapper \ Collaborator"Nej. Istället för
StdClass
vi får en förekomst avPhpSpec \ Wrapper \ samarbetspartners
. Vad handlar det här om?Tycka om
Ämne
,Medarbetare
är en omslag och implementerarWrapperInterface
. Det sveper en förekomst av\ Prophecy \ Prophecy \ ObjectProphecy
, som härstammar från profetian, det mocking-ramverket som kommer samman med PhpSpec. Istället för enStdClass
Exempelvis, PhpSpec ger oss en mock. Detta gör skrattande lätt med PhpSpec och tillåter oss att lägga till löften till våra föremål så här:$ User-> getAge () -> willreturn (10); $ This-> setUser ($ user); $ This-> getUserStatus () -> shouldReturn ( 'barn');Med denna korta rundtur i delar av PhpSpecs internals, hoppas jag att det är mer än en enkel testram.
Skillnaden mellan TDD och BDD
PhpSpec är ett verktyg för att göra SpecBDD, så för att få en bättre förståelse, låt oss ta en titt på skillnaderna mellan testdriven utveckling (TDD) och beteendedriven utveckling (BDD). Därefter tar vi en snabb titt på hur PhpSpec skiljer sig från andra verktyg som PHPUnit.
TDD är konceptet att låta automatiska tester driva design och implementering av kod. Genom att skriva små tester för varje funktion, innan vi faktiskt genomför dem, vet vi att vår kod uppfyller den specifika funktionen när vi får ett godkänt test. Med ett godkänt test, efter refactoring, stoppar vi kodning och skriver nästa test istället. Mantra är "röd", "grön", "refaktor"!
BDD har sitt ursprung från - och liknar mycket - TDD. Ärligt talat är det främst en fråga om formulering, vilket verkligen är viktigt eftersom det kan förändra hur vi tänker som utvecklare. När TDD talar om testning, talar BDD om att beskriva beteende.
Med TDD fokuserar vi på att verifiera att vår kod fungerar som vi förväntar oss att den ska fungera, medan vi med BDD fokuserar på att verifiera att vår kod faktiskt beter sig som vi vill ha det. En viktig orsak till framväxten av BDD, som ett alternativ till TDD, är att undvika att använda ordet "test". Med BDD är vi inte riktigt intresserade av att testa implementeringen av vår kod, vi är mer intresserade av att testa vad det gör (dess beteende). När vi gör BDD, istället för TDD, har vi historier och specifikationer. Dessa gör att traditionella test är överflödiga.
Berättelser och specifikationer är nära knutna till projektets intressenters förväntningar. Skrivningsberättelser (med ett verktyg som Behat) skulle helst ske tillsammans med intressenterna eller domänexperterna. Berättelserna täcker det yttre beteendet. Vi använder specs för att utforma det interna beteendet som behövs för att fylla i stegen i berättelserna. Varje steg i en berättelse kan kräva flera iterationer med att skriva specifikationer och implementeringskod, innan den är nöjd. Våra berättelser, tillsammans med våra specs, hjälper oss att se till att vi inte bara bygger en fungerande sak, men att det också är det rätta. Som så har BDD mycket att göra med kommunikation.
Hur är PhpSpec annorlunda än PHPUnit?
För några månader sedan skrev en anmärkningsvärd medlem av PHP-gemenskapen, Mathias Verraes, "En enhet för testning av enheter i en tweet" på Twitter. Poängen var att passa källkoden för en funktionell testningsram till en enda tweet. Som du kan se från kärnan, är koden verkligen funktionell och tillåter dig att skriva grundläggande enhetstester. Begreppet enhetstestning är faktiskt ganska enkelt: Kontrollera någon form av påstående och meddela användaren av resultatet.
Naturligtvis är de flesta testramar som PHPUnit faktiskt mycket mer avancerade och kan göra mycket mer än Mathias ramar, men det visar fortfarande en viktig punkt: Du hävdar någonting och då ramar din ram det påståendet för dig.
Låt oss ta en titt på ett mycket grundläggande PHPUnit-test:
public function testTrue () $ this-> assertTrue (false);Skulle du kunna skriva en super enkel implementering av ett testramverk som kan köra detta test? Jag är ganska säker på att svaret är "ja" du kan göra det. När allt är det enda
assertTrue ()
Metod har att göra är att jämföra ett värde motSann
och kasta ett undantag om det misslyckas. Vad som händer är faktiskt ganska rakt framåt.Så hur är PhpSpec annorlunda? Först och främst är PhpSpec inte ett testverktyg. Att testa din kod är inte huvudmålet för PhpSpec, men det blir en bieffekt om du använder den för att designa din programvara genom att stegvis lägga till specifikationer för beteendet (BDD).
För det andra anser jag att ovanstående avsnitt borde ha gjort klart hur PhpSpec är annorlunda. Låt oss ändå jämföra en kod:
// PhpSpec funktion it_is_initializable () $ this-> shouldHaveType ('Suhm \ HelloWorld'); // PHPUnit funktion testIsInitializable () $ object = new Suhm \ HelloWorld (); $ this-> assertInstanceOf ('Suhm \ HelloWorld', $ object);Eftersom PhpSpec är mycket uppfattat och gör några påståenden om hur vår kod är utformad, ger den oss en väldigt enkel metod att beskriva vår kod. Å andra sidan gör PHPUnit inga påståenden mot vår kod och låter oss göra ganska mycket vad vi vill ha. I grund och botten alla PHPUnit gör för oss i det här exemplet, är att springa
$ object
motinstans av
operatör.Även om PHPUnit kan tyckas lättare att komma igång med (jag tror inte det är), om du inte är försiktig kan du lätt falla i fällor av dålig design och arkitektur eftersom det gör att du kan göra nästan vad som helst. Med det sagt kan PHPUnit fortfarande vara bra för många användningsfall, men det är inte ett designverktyg som PhpSpec. Det finns ingen vägledning - du måste veta vad du gör.
PhpSpec: Ett designverktyg
Från PhpSpec webbplats kan vi lära oss att PhpSpec är:
En php verktygssats för att driva framväxande design genom specifikation.Låt mig säga det en gång till: PhpSpec är inte en testram. Det är ett utvecklingsverktyg. Ett programvaruverktyg. Det är inte en enkel påstående ram som jämför värden och kastar undantag. Det är ett verktyg som hjälper oss att utforma och bygga väl utformad kod. Det kräver att vi tänker på strukturen i vår kod och verkställer vissa arkitektoniska mönster, där en klass kartläggs till en spec. Om du bryter principen om ansvar för ensam ansvar och behöver delvis missa något, får du inte göra det.
Glad spec'ing!
åh! Och slutligen = eftersom PhpSpec själv är speced, föreslår jag att du går till GitHub och utforskar källan för att lära dig mer.