Se si confronta PhpSpec con altri framework di test, si scoprirà che si tratta di uno strumento molto sofisticato e motivato. Uno dei motivi di ciò è che PhpSpec non è un framework di test come quelli che già conosci.
Invece, è uno strumento di progettazione che aiuta a descrivere il comportamento del software. Un effetto collaterale della descrizione del comportamento del software con PhpSpec, è che si finirà con le specifiche che serviranno anche come test in seguito.
In questo articolo, daremo un'occhiata sotto il cofano di PhpSpec e cercheremo di capire meglio come funziona e come usarlo.
Se vuoi rispolverare su phpspec, dai un'occhiata al mio tutorial introduttivo.
Iniziamo guardando alcuni dei concetti e delle classi chiave che formano PhpSpec.
$ questo
Capire cosa $ questo
si riferisce a è la chiave per capire come PhpSpec si differenzia da altri strumenti. Fondamentalmente, $ questo
fare riferimento a un'istanza della classe reale sottoposta a test. Proviamo a indagare un po 'di più per capire meglio cosa intendiamo.
Prima di tutto, abbiamo bisogno di una specifica e una classe con cui giocare. Come sapete, i generatori di PhpSpec rendono questo super facile per noi:
$ phpspec desc "Suhm \ HelloWorld" $ phpspec run Vuoi che crei 'Suhm \ HelloWorld' per te? y
Successivamente, apri il file spec generato e proviamo a ottenere un po 'più di informazioni $ questo
:
shouldHaveType ( 'Suhm \ HelloWorld'); var_dump (get_class ($ this));
get_class ()
restituisce il nome della classe di un dato oggetto. In questo caso, ci limitiamo a lanciare $ questo
lì dentro per vedere cosa restituisce:
$ string (24) "spec \ Suhm \ HelloWorldSpec"
Ok, quindi non troppo sorprendentemente, get_class ()
ce lo dice $ questo
è un'istanza di spec \ Suhm \ HelloWorldSpec
. Questo ha senso poiché, dopo tutto, questo è appena semplice codice PHP vecchio. Se invece lo abbiamo usato get_parent_class ()
, vorremmo ottenerePhpSpec \ ObjectBehavior
, poiché la nostra specifica estende questa classe.
Ricorda, te l'ho appena detto $ questo
effettivamente riferito alla classe sotto test, che sarebbeSuhm \ HelloWorld
nel nostro caso? Come puoi vedere, il valore di ritorno di get_class ($ this)
è in contraddizione con $ This-> shouldHaveType ( 'Suhm \ HelloWorld');
.
Proviamo qualcos'altro:
shouldHaveType ( 'Suhm \ HelloWorld'); var_dump (get_class ($ this)); $ This-> dumpThis () -> shouldReturn ( 'spec \ Suhm \ HelloWorldSpec');
Con il codice sopra, proviamo a chiamare un metodo chiamato dumpThis ()
sul Ciao mondo
esempio. Colleghiamo un'aspettativa alla chiamata al metodo, prevedendo che il valore di ritorno della funzione sia una stringa che contiene"Spec \ Suhm \ HelloWorldSpec"
. Questo è il valore di ritorno da get_class ()
sulla linea sopra.
Ancora una volta, i generatori PhpSpec possono aiutarci con alcune impalcature:
$ phpspec run Vuoi che crei 'Suhm \ HelloWorld :: dumpThis ()' per te? y
Proviamo a chiamare get_class ()
dall'interno dumpThis ()
pure:
Ancora una volta, non sorprendentemente, otteniamo:
10 ✘ è inizializzabile prevedibile "spec \ Suhm \ HelloWorldSpec", ma ha ottenuto "Suhm \ HelloWorld".Sembra che ci manchi qualcosa qui. Ho iniziato dicendo questo
$ questo
non si riferisce a quello che pensi che faccia, ma finora i nostri esperimenti non hanno mostrato nulla di inaspettato. Tranne una cosa: come potremmo chiamare$ This-> dumpThis ()
prima che esistesse senza PHP squittire contro di noi?Per capirlo, dobbiamo immergerci nel codice sorgente di PhpSpec. Se vuoi dare un'occhiata da solo, puoi leggere il codice su GitHub.
Dai uno sguardo al seguente codice da
src / PhpSpec / ObjectBehavior.php
(la classe che estende la nostra specifica):/ ** * I proxy chiamano tutti all'oggetto PhpSpec * * @param stringa $ metodo * @param array $ argomenti * * @return misto * / public function __call ($ method, array $ arguments = array ()) return call_user_func_array ( array ($ this-> object, $ method), $ argomenti);I commenti danno la maggior parte di esso:
"I proxy chiamano tutti all'oggetto PhpSpec"
. Il PHP__chiamata
metodo è un metodo magico chiamato automaticamente ogni volta che un metodo non è accessibile (o non esistente).Questo significa che quando abbiamo provato a chiamare
$ This-> dumpThis ()
, la chiamata è stata apparentemente inoltrata al soggetto PhpSpec. Se si guarda il codice, è possibile vedere che la chiamata al metodo è inoltrata a$ This-> oggetto
. (Lo stesso vale per le proprietà della nostra istanza, che sono tutte trasmesse anche al soggetto, usando altri metodi magici. Dai uno sguardo alla fonte per vedere di persona.)Consultiamoci
get_class ()
ancora una volta e vedere cosa ha da dire$ This-> oggetto
:shouldHaveType ( 'Suhm \ HelloWorld'); var_dump (get_class ($ this-> oggetto));E guarda cosa otteniamo:
string (23) "PhpSpec \ Wrapper \ Subject"Più su
Soggetto
Soggetto
è un involucro e implementa ilPhpSpec \ Wrapper \ WrapperInterface
. È una parte fondamentale di PhpSpec e consente tutta la [apparentemente] magia che il framework può fare. Racchiude un'istanza della classe che stiamo testando, in modo che possiamo fare ogni genere di cose come chiamare metodi e proprietà che non esistono e impostare aspettative.Come accennato, PhpSpec è molto convinto su come scrivere e specificare il codice. Una mappa specifica per una classe. Hai solo uno soggetto per specifica, che PhpSpec avvolgerà con cura per te. La cosa importante da notare su questo è che questo ti permette di usare
$ questo
come se fosse l'istanza attuale e rende le specifiche veramente leggibili e significative.PhpSpec contiene a
involucro
che si prende cura di istanziare ilSoggetto
. Imballa ilSoggetto
con l'oggetto reale che stiamo specificando. DaSoggetto
implementa ilWrapperInterface
deve avere ungetWrappedObject ()
metodo che ci dà accesso all'oggetto. Questa è l'istanza dell'oggetto con cui stavamo cercando in precedenzaget_class ()
.Proviamoci di nuovo:
shouldHaveType ( 'Suhm \ HelloWorld'); var_dump (get_class ($ this-> object> getWrappedObject ())); // E per essere completamente sicuro: var_dump ($ this-> object-> getWrappedObject () -> dumpThis ());E qui vai:
$ vendor / bin / phpspec run stringa (15) "Suhm \ HelloWorld" stringa (15) "Suhm \ HelloWorld"Anche se molte cose stanno accadendo dietro la scena, alla fine stiamo ancora lavorando con l'istanza dell'oggetto reale di
Suhm \ HelloWorld
. Tutto bene.In precedenza, quando abbiamo chiamato
$ This-> dumpThis ()
, abbiamo appreso come la chiamata è stata effettivamente inoltrata alSoggetto
. L'abbiamo anche imparatoSoggetto
è solo un wrapper e non l'oggetto reale.Con questa conoscenza, è chiaro che non siamo in grado di chiamare
dumpThis ()
sopraSoggetto
senza un altro metodo magico.Soggetto
ha un__chiamata()
metodo pure:/ ** * @param stringa $ metodo * @param array $ argomenti * * @return misto | Oggetto * / funzione pubblica __call ($ metodo, array $ argomenti = array ()) if (0 === strpos ($ metodo , 'should')) return $ this-> callExpectation (metodo $, $ argomenti); return $ this-> caller-> call ($ metodo, $ argomenti);Questo metodo fa una delle due cose. Innanzitutto, controlla se il nome del metodo inizia con "dovrebbe". Se lo fa, è un'aspettativa e la chiamata è delegata a un metodo chiamato
callExpectation ()
. In caso contrario, la chiamata viene invece delegata a un'istanza diPhpSpec \ Wrapper \ Soggetto \ chiamante
.Ignoreremo il
visitatore
per adesso. Anch'esso contiene l'oggetto avvolto e sa come chiamare i metodi su di esso. Ilvisitatore
restituisce un'istanza spostata quando chiama i metodi sull'argomento, consentendoci di concatenare le aspettative ai metodi, come abbiamo fatto condumpThis ()
.Invece, diamo un'occhiata al
callExpectation ()
metodo:/ ** * @param stringa $ metodo * @param array $ argomenti * * @return misto * / funzione privata callExpectation (metodo $, array $ argomenti) $ subject = $ this-> makeSureWeHaveASubject (); $ expectation = $ this-> expectationFactory-> create ($ method, $ subject, $ argomenti); if (0 === strpos ($ method, 'shouldNot')) return $ expectation-> match (lcfirst (substr ($ method, 9)), $ this, $ arguments, $ this-> wrappedObject); return $ expectation-> match (lcfirst (substr ($ method, 6)), $ this, $ arguments, $ this-> wrappedObject);Questo metodo è responsabile della creazione di un'istanza di
PhpSpec \ Wrapper \ Oggetto \ Expectation \ ExpectationInterface
. Questa interfaccia detta aincontro()
metodo, che ilcallExpectation ()
chiama per verificare le aspettative. Ci sono quattro diversi tipi di aspettative:Positivo
,Negativo
,PositiveThrow
eNegativeThrow
. Ciascuna di queste aspettative contiene un'istanza diPhpSpec \ Matcher \ MatcherInterface
che ilincontro()
usi del metodo. Diamo un'occhiata ai giocatori di seguito.matchers
I Matchers sono ciò che utilizziamo per determinare il comportamento dei nostri oggetti. Ogni volta che scriviamo
dovrebbero…
onon dovrebbe…
, stiamo usando un matcher. È possibile trovare un elenco completo di abbinamenti PhpSpec sul mio blog personale.Ci sono molti abbinamenti inclusi con PhpSpec, che amplia tutti
PhpSpec \ Matcher \ BasicMatcher
classe, che implementa ilMatcherInterface
. Il modo in cui gli abbinatori funzionano è piuttosto semplice. Diamo un'occhiata a questo insieme e ti incoraggio a dare un'occhiata anche al codice sorgente.Ad esempio, diamo un'occhiata a questo codice dal
IdentityMatcher
:/ ** * @var array * / private static $ keywords = array ('return', 'be', 'equal', 'beEqualTo'); / ** * @param stringa $ nome * @param mixed $ subject * @param array $ argomenti * * @return bool * / supporta la funzione pubblica ($ name, $ subject, array $ argomenti) return in_array ($ name, self :: $ parole chiave) && 1 == count ($ argomenti);Il
supporti ()
il metodo è dettato dalMatcherInterface
. In questo caso, quattro alias sono definiti per il matcher in$ parole chiave
array. Ciò consentirà al matcher di supportare:shouldReturn ()
,dovrebbe essere()
,shouldEqual ()
oshouldBeEqualTo ()
, oshouldNotReturn ()
,non dovrebbe essere()
,shouldNotEqual ()
oshouldNotBeEqualTo ()
.Dal
BasicMatcher
, due metodi sono ereditati:positiveMatch ()
enegativeMatch ()
. Sembrano così:/ ** * @param stringa $ nome * @param mixed $ subject * @param array $ argomenti * * @return misto * * @throws FailureException * / finale public function positiveMatch ($ name, $ subject, array $ arguments) if (false === $ this-> matches ($ subject, $ arguments)) throw $ this-> getFailureException ($ name, $ subject, $ arguments); return $ subject;Il
positiveMatch ()
metodo lancia un'eccezione se ilpartite ()
metodo (metodo astratto che i concorrenti devono implementare) restituiscefalso
. IlnegativeMatch ()
il metodo funziona in modo opposto. Ilpartite ()
metodo per ilIdentityMatcher
usa il===
operatore per confrontare il$ subject
con l'argomento fornito per il metodo matcher:/ ** * @param mixed $ subject * @param array $ argomenti * * @return bool * / corrisponde a funzioni protette ($ subject, array $ argomenti) return $ subject === $ arguments [0];Potremmo usare il matcher in questo modo:
$ This-> getUser () -> shouldNotBeEqualTo ($ anotherUser);Che alla fine chiamerebbe
negativeMatch ()
e assicurati chepartite ()
restituisce falso.Dai un'occhiata ad alcuni degli altri giocatori e guarda cosa fanno!
Promesse di più magia
Prima di concludere questo breve tour degli interni di PhpSpec, diamo un'occhiata a un altro pezzo di magia:
shouldHaveType ( 'Suhm \ HelloWorld'); var_dump (get_class ($ object));Aggiungendo il tipo suggerito
$ oggetto
parametro per il nostro esempio, PhpSpec utilizzerà automaticamente il reflection per iniettare un'istanza della classe da utilizzare. Ma con le cose che abbiamo già visto, ci fidiamo davvero di avere un'istanza verastdClass
? Consultiamociget_class ()
un'altra volta:$ vendor / bin / phpspec run string (28) "PhpSpec \ Wrapper \ Collaborator"No. Invece di
stdClass
otteniamo un'istanza diPhpSpec \ Wrapper \ Collaboratore
. Cosa riguarda?Piace
Soggetto
,Collaboratore
è un involucro e implementa ilWrapperInterface
. Include un'istanza di\ Prophecy \ Prophecy \ ObjectProphecy
, che deriva da Prophecy, la struttura derisoria che si unisce a PhpSpec. Invece di unstdClass
istanza, PhpSpec ci dà una finta. Questo rende ridicolo facilmente ridicolo con PhpSpec e ci consente di aggiungere promesse ai nostri oggetti in questo modo:$ User-> getAge () -> willreturn (10); $ This-> setuser ($ user); $ This-> getUserStatus () -> shouldReturn ( 'bambino');Con questo breve tour di parti degli interni di PhpSpec, spero che vedrete che è più di un semplice framework di test.
La differenza tra TDD e BDD
PhpSpec è uno strumento per fare SpecBDD, quindi per avere una migliore comprensione, diamo un'occhiata alle differenze tra lo sviluppo guidato da test (TDD) e lo sviluppo guidato dal comportamento (BDD). In seguito, daremo una rapida occhiata a come PhpSpec si differenzia da altri strumenti come PHPUnit.
TDD è il concetto di lasciare che i test automatizzati guidino la progettazione e l'implementazione del codice. Scrivendo piccoli test per ogni funzione, prima di implementarli, quando otteniamo un test di passaggio, sappiamo che il nostro codice soddisfa quella specifica funzione. Con un test di passaggio, dopo il refactoring, smettiamo di scrivere codice e scriviamo invece il prossimo test. Il mantra è "rosso", "verde", "refactoring"!
BDD ha la sua origine da - ed è molto simile a - TDD. Onestamente, è principalmente una questione di testo, che è davvero importante dal momento che può cambiare il modo in cui pensiamo come sviluppatori. Laddove TDD parla di test, BDD parla della descrizione del comportamento.
Con TDD ci concentriamo a verificare che il nostro codice funzioni nel modo in cui ci aspettiamo che funzioni, mentre con BDD ci concentriamo a verificare che il nostro codice si comporti effettivamente nel modo in cui lo vogliamo. Una delle ragioni principali dell'emergere del BDD, come alternativa al TDD, è evitare di usare la parola "test". Con BDD non siamo realmente interessati a testare l'implementazione del nostro codice, siamo più interessati a testare ciò che fa (il suo comportamento). Quando facciamo BDD, invece di TDD, abbiamo storie e specifiche. Ciò rende ridondanti i test tradizionali di scrittura.
Le storie e le specifiche sono strettamente legate alle aspettative degli stakeholder del progetto. Scrivere storie (con uno strumento come Behat), preferibilmente avverrà insieme agli stakeholder o agli esperti di dominio. Le storie coprono il comportamento esterno. Usiamo le specifiche per progettare il comportamento interno necessario per completare i passaggi delle storie. Ogni passaggio di una storia potrebbe richiedere più iterazioni con specifiche di scrittura e codice di implementazione, prima che sia soddisfatto. Le nostre storie, insieme alle nostre specifiche, ci aiutano a garantire che non solo stiamo costruendo una cosa funzionante, ma che è anche la cosa giusta. In questo modo, BDD ha molto a che fare con la comunicazione.
In che modo PhpSpec è diverso da PHPUnit?
Alcuni mesi fa, un membro importante della comunità PHP, Mathias Verraes, ha pubblicato su Twitter "Un quadro di test unitario in un tweet". Il punto era quello di adattare il codice sorgente di una struttura di testing dell'unità funzionale in un singolo tweet. Come puoi vedere dal succo, il codice è veramente funzionale e ti permette di scrivere test unitari di base. Il concetto di unità di test è in realtà piuttosto semplice: controlla una sorta di asserzione e notifica all'utente il risultato.
Certamente, molti framework di testing, come PHPUnit, sono in effetti molto più avanzati e possono fare molto di più del framework di Mathias, ma mostra comunque un punto importante: asserisci qualcosa e quindi il framework esegue quell'asserzione per te.
Diamo un'occhiata a un test PHPUnit molto semplice:
funzione pubblica testTrue () $ this-> assertTrue (false);Sareste in grado di scrivere un'implementazione super semplice di un framework di test in grado di eseguire questo test? Sono abbastanza sicuro che la risposta sia "sì" che potresti farlo. Dopo tutto, l'unica cosa
assertTrue ()
il metodo deve fare è confrontare un valore controvero
e lanciare un'eccezione se fallisce. In sostanza, ciò che sta accadendo è in realtà piuttosto semplice.Quindi, come è diverso PhpSpec? Innanzitutto, PhpSpec non è uno strumento di test. Il test del codice non è l'obiettivo principale di PhpSpec, ma diventa un effetto collaterale se lo si utilizza per progettare il software aggiungendo in modo incrementale specifiche per il comportamento (BDD).
In secondo luogo, penso che le sezioni precedenti avrebbero già chiarito in che modo PhpSpec è diverso. Tuttavia, confrontiamo qualche codice:
// Funzione PhpSpec it_is_initializable () $ this-> shouldHaveType ('Suhm \ HelloWorld'); // Funzione PHPUnit testIsInitializable () $ object = new Suhm \ HelloWorld (); $ this-> assertInstanceOf ('Suhm \ HelloWorld', $ oggetto);Poiché PhpSpec è altamente motivato e fa alcune asserzioni su come il nostro codice è stato progettato, ci offre un modo molto semplice per descrivere il nostro codice. D'altra parte, PHPUnit non fa alcuna asserzione verso il nostro codice e ci permette di fare praticamente ciò che vogliamo. Fondamentalmente tutto ciò che PHPUnit fa per noi in questo esempio, è l'esecuzione
$ oggetto
contro ilinstanceof
operatore.Anche se PHPUnit potrebbe sembrare più facile iniziare (non credo che lo sia), se non si presta attenzione, si può facilmente cadere in trappole di cattiva progettazione e architettura perché consente di fare quasi tutto. Detto questo, PHPUnit può ancora essere ottimo per molti casi d'uso, ma non è uno strumento di progettazione come PhpSpec. Non c'è una guida: devi sapere cosa stai facendo.
PhpSpec: uno strumento di progettazione
Dal sito Web PhpSpec, possiamo scoprire che PhpSpec è:
Un set di strumenti php per guidare la progettazione emergente secondo le specifiche.Lasciatemelo dire ancora una volta: PhpSpec non è un framework di test. È uno strumento di sviluppo. Uno strumento di progettazione software. Non è un semplice quadro di asserzioni che confronta valori e genera eccezioni. È uno strumento che ci assiste nel progettare e costruire codice ben fatto. Ci impone di pensare alla struttura del nostro codice e di applicare determinati schemi architettonici, in cui una classe esegue il mapping su una specifica. Se infrangi il principio di responsabilità individuale e hai bisogno di deridere parzialmente qualcosa, non ti sarà permesso farlo.
Spec'ing felice!
Oh! E infine, poiché speci fi che lo stesso PhpSpec è specifico, ti suggerisco di andare su GitHub ed esplorare la fonte per saperne di più.