Förstå PhpSpec

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.

I den här artikeln…

  • En snabb tur av PhpSpec Internals
  • Skillnaden mellan TDD och BDD
  • Hur är PhpSpec annorlunda (från PHPUnit)
  • PhpSpec: Ett designverktyg

En snabb tur av PhpSpec Internals

Låt oss börja med att titta på några av de nyckelbegrepp och klasser som bildar PhpSpec.

Förståelse $ 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 implementerar PhpSpec \ 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 implementerar WrapperInterface det måste ha en getWrappedObject ()metod som ger oss tillgång till objektet. Det här är objektet som vi letade efter tidigare med get_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 av PhpSpec \ 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. De Uppringare returnerar en wrapped instans när det kallar metoder på ämnet, så att vi kan kedja förväntningar på metoder, som vi gjorde med dumpThis ().

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 a match() metod, som callExpectation () samtal för att kontrollera förväntan. Det finns fyra olika slags förväntningar: PositivNegativPositiveThrow och NegativeThrow. Var och en av dessa förväntningar innehåller en förekomst av PhpSpec \ Matcher \ MatcherInterface Att den match() 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…  eller borde 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 implementerar MatcherInterface. 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 av MatcherInterface. 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 (), eller shouldNotReturn ()bör inte vara()shouldNotEqual () eller shouldNotBeEqualTo ().

Från BasicMatcher, två metoder ärva: positiveMatch () och negativeMatch (). 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 $ subject  

De positiveMatch () Metoden kastar ett undantag om tändstickor() metod (abstrakt metod som matcharna måste genomföra) returnerar falsk. De negativeMatch () Metoden fungerar motsatt sätt. De tä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 att tä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 av StdClass? Låt oss samråda get_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 av PhpSpec \ Wrapper \ samarbetspartners. Vad handlar det här om?

Tycka om ÄmneMedarbetare är en omslag och implementerar WrapperInterface. 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 en StdClass 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 mot Sann 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.