Înțelegerea PhpSpec

Dacă comparați PhpSpec cu alte cadre de testare, veți găsi că acesta este un instrument foarte sofisticat și încrezător. Unul dintre motivele pentru aceasta este faptul că PhpSpec nu este un cadru de testare precum cele pe care le cunoașteți deja. 

În schimb, este un instrument de proiectare care ajută la descrierea comportamentului software-ului. Un efect secundar al descrierii comportamentului software-ului cu PhpSpec este că veți termina cu specificații care vor servi și ca teste ulterioare.

În acest articol, vom examina sub capota PhpSpec și vom încerca să înțelegem mai bine modul în care funcționează și cum se utilizează.

Dacă doriți să vă spălați pe phpspec, aruncați o privire la tutorialul meu de început.

În acest articol…

  • Un tur rapid al PhpSpec Internals
  • Diferența dintre TDD și BDD
  • Cum este diferit PhpSpec (de la PHPUnit)
  • PhpSpec: Un instrument de proiectare

Un tur rapid al PhpSpec Internals

Să începem prin analizarea unor concepte și clase cheie care formează PhpSpec.

Înţelegere $ this

Înțelegerea a ceea ce $ this se referă la cheia pentru a înțelege modul în care PhpSpec diferă de alte instrumente. Pe scurt, $ this se referă la o instanță a clasei reale testate. Să încercăm să investigăm mai mult acest lucru pentru a înțelege mai bine ce înțelegem.

Mai întâi de toate, avem nevoie de o spec și o clasă pentru a juca cu. După cum știți, generatoarele PhpSpec fac acest lucru foarte ușor pentru noi:

$ phpspec desc "Suhm \ HelloWorld" $ phpspec run Doriți să vă creez "Suhm \ HelloWorld" pentru tine? y 

În continuare, deschideți fișierul spec. Generat și să încercăm să obținem mai multe informații despre el $ this:

shouldHaveType ( 'Suhm \ HelloWorld'); var_dump (get_class ($ acest));  

get_class () returnează numele clasei unui obiect dat. În acest caz, aruncam doar $ this acolo pentru a vedea ce se întoarce:

$ șir (24) "spec. \ Suhm \ HelloWorldSpec"

Bine, deci nu prea surprinzător, get_class () ne spune asta $ this este un exemplu de spec \ Suhm \ HelloWorldSpec. Acest lucru are sens, deoarece, la urma urmei, aceasta este doar cod vechi PHP vechi. Dacă am folosit-o get_parent_class (), ne-ar luaPhpSpec \ ObjectBehavior, deoarece specificațiile noastre extind această clasă.

Ține minte, tocmai ți-am spus asta $ this se referă efectiv la clasa de test, care ar fiSuhm \ HelloWorld în cazul nostru? După cum puteți vedea, valoarea de retur a get_class ($ acest) este în contradicție cu $ This-> shouldHaveType ( 'Suhm \ HelloWorld');.

Să încercăm altceva:

shouldHaveType ( 'Suhm \ HelloWorld'); var_dump (get_class ($ acest)); $ This-> dumpThis () -> shouldReturn ( 'spec \ Suhm \ HelloWorldSpec');  

Cu codul de mai sus, încercăm să apelam o metodă numită dumpThis () pe Salut Lume instanță. Ne lansează o așteptare la apelul metodei, așteptând ca valoarea returnată a funcției să fie un șir care conține"Spec \ Suhm \ HelloWorldSpec". Aceasta este valoarea returnată de la get_class () pe linia de mai sus.

Din nou, generatoarele PhpSpec ne pot ajuta cu niște schele:

$ phpspec run Doriți să creez 'Suhm \ HelloWorld :: dumpThis ()' pentru tine? y 

Să încercăm să sunăm get_class () din cadrul dumpThis () de asemenea:

Din nou, nu este surprinzător, obținem:

 10 ✘ este inițializabil "spec \ Suhm \ HelloWorldSpec", dar a primit "Suhm \ HelloWorld". 

Se pare că ne lipsește ceva aici. Am început să vă spun asta $ this nu se referă la ceea ce credeți că are, dar până acum experimentele noastre nu au arătat nimic neașteptat. Cu excepția unui singur lucru: Cum am putea să sunăm $ This-> dumpThis () înainte ca acesta să existe fără ca PHP să ne scurgă?

Pentru a înțelege acest lucru, trebuie să ne scufundăm în codul sursă PhpSpec. Dacă vrei să te uiți singur, poți citi codul pe GitHub.

Aruncați o privire de la următorul cod de la src / PhpSpec / ObjectBehavior.php (clasa pe care specia noastră o extinde):

/ * * Proxies toate apel la subiectul PhpSpec * * @param string $ metoda * @param array $ argumente * * @return amestecat * / funcția publică __call ($ metoda, array $ argumente = array ()) return call_user_func_array ( array ($ this-> object, metoda $), argumente $);  

Comentariile dau cea mai mare parte a acesteia: "Proxy-urile fac apel la subiectul PhpSpec". PHP __apel metoda este o metodă magică numită automat ori de câte ori o metodă nu este accesibilă (sau nu există). 

Asta înseamnă că atunci când am încercat să sunăm $ This-> dumpThis (), apelul a fost aparent proxy pentru subiectul PhpSpec. Dacă te uiți la cod, poți vedea că apelul metodei este proxy $ This-> obiect. (Același lucru se întâmplă și pentru proprietățile din instanța noastră, care sunt toate proxy la subiect, folosind alte metode magice.) Aruncați o privire în sursă pentru a vă vedea.)

Să ne consultăm get_class () încă o dată și să vedem ce are de spus $ This-> obiect:

shouldHaveType ( 'Suhm \ HelloWorld'); var_dump (get_class ($ this-> obiect));  

Și uite ce avem:

șir (23) "PhpSpec \ Wrapper \ Subject"

Mai multe despre Subiect

Subiect este un înveliș și implementează PhpSpec \ Wrapper \ WrapperInterface. Este o parte esențială a PhpSpec și permite toate magiile [aparent] pe care le poate face cadrul. Elimină o instanță a clasei pe care o testăm, astfel încât să putem face tot felul de lucruri, cum ar fi metodele de apelare și proprietățile care nu există și care să stabilească așteptările. 

După cum sa menționat, PhpSpec este foarte mulțumit de modul în care trebuie să scrieți și să vă specificați codul. Un spec hărți la o clasă. Numai tu ai unu subiectul pe spec., pe care PhpSpec îl va înfășura cu atenție. Lucrul important pe care îl aveți în vedere este că acest lucru vă permite să utilizați $ this ca și cum ar fi fost instanța reală și face pentru specificații cu adevărat lizibile și semnificative.

PhpSpec conține a ambalaj care se ocupă de crearea instanței Subiect. Împachetează Subiect cu obiectul propriu pe care îl interpretăm. De cand Subiect implementează WrapperInterface trebuie să aibă a getWrappedObject ()metodă care ne oferă acces la obiect. Aceasta este instanța obiectului cu care căutam mai devreme get_class ()

Să încercăm din nou:

shouldHaveType ( 'Suhm \ HelloWorld'); var_dump (get_class ($ this-> object-> getWrappedObject ())); // Și doar pentru a fi complet sigur: var_dump ($ this-> object-> getWrappedObject () -> dumpThis ());  

Și acolo te duci:

$ vendor / bin / phpspec șirul de șir (15) "Suhm \ HelloWorld" șir (15) "Suhm \ HelloWorld" 

Chiar dacă multe lucruri se întâmplă în spatele scenei, în cele din urmă lucrăm încă cu instanța obișnuită a obiectului Suhm \ HelloWorld. Totul e bine.

Anterior, când am sunat $ This-> dumpThis (), am aflat cum apelul a fost de fapt proxy către Subiect. De asemenea, am învățat asta Subiect este doar un înveliș și nu obiectul real. 

Cu aceste cunoștințe, este clar că nu suntem capabili să sunăm dumpThis () pe Subiect fără altă metodă magică. Subiect are o __apel() și metoda:

/ * * @param string $ metoda * @param array $ argumente * * @return amestecat * Subiect * / funcția publică __call ($ method, array $ argumente = array ()) if (0 === strpos , 'ar trebui')) retur $ this-> callExpectation (metoda $, argumente $);  returnați $ this-> caller-> apel (metoda $, argumente $);  

Această metodă are unul din cele două lucruri. În primul rând, verifică dacă numele metodei începe cu "ar trebui". În caz contrar, este o așteptare, iar apelul este delegat unei metode numite callExpectation (). Dacă nu, apelul este în schimb delegat la o instanță de PhpSpec \ Wrapper \ Subiect \ apelant

Vom ignora apelant deocamdata. De asemenea, conține obiectul înfășurat și știe cum să numească metode pe el. apelant returnează o instanță înfășurată atunci când solicită metode pe această temă, permițându-ne să așteptăm lanțurile așteptate la metode, așa cum am făcut-o cu noi dumpThis ().

În schimb, să aruncăm o privire la callExpectation () metodă:

/ ** * @param string $ metoda * @param array $ argumente * * @return amestecat * / funcția privată callExpectation ($ method, array $ arguments) $ subject = $ this-> makeSureWeHaveASubject (); $ expectation = $ this-> expectationFactory-> create ($ metoda, $ subiect, $ argumente); dacă (0 === strpos (metoda $, "shouldNot")) return $ expectation-> match (lcfirst (substr ($ metoda, 9)), $ this, $ arguments, $ this-> wrappedObject);  return $ expectation-> match (lcfirst (substr ($ metoda, 6)), $ this, $ argumente, $ this-> wrappedObject);  

Această metodă este responsabilă pentru construirea unui exemplu de PhpSpec \ Wrapper \ Subiect \ Așteptări \ ExpectationInterface. Această interfață impune a Meci() metoda, care callExpectation () solicită să verifice așteptările. Există patru tipuri diferite de așteptări: PozitivNegativPositiveThrow și NegativeThrow. Fiecare dintre aceste așteptări conține un exemplu de PhpSpec \ Matcher \ MatcherInterface că Meci() foloseste metoda. Să ne uităm la următorii oameni.

matchers

Matches sunt ceea ce folosim pentru a determina comportamentul obiectelor noastre. Ori de câte ori scriem ar trebui să…  sau nu ar trebui… , noi folosim o potrivire. Puteți găsi o listă cuprinzătoare de medici PhpSpec pe blogul meu personal.

Există numeroși colaboratori incluși în PhpSpec, dintre care toate se extind PhpSpec \ Matcher \ BasicMatcher clasa, care implementează MatcherInterface. Modul în care lucrează angajații este destul de drept. Să aruncăm o privire la ea împreună și vă încurajez să aruncați o privire la codul sursă, de asemenea.

De exemplu, să examinăm acest cod de la IdentityMatcher:

/ ** * @var array * / static privat $ keywords = array ('return', 'be', 'equal', 'beEqualTo'); / ** * @param string $ nume * @ param amestecat $ subiect * @param array $ argumente * * @return bool * / suport public functie ($ nume, $ subiect, array $ argumente) return in_array :: $ keywords) && 1 == count ($ argumente);  

 suporturi () metoda este dictată de MatcherInterface. În acest caz, patru pseudonime sunt definite pentru matcher în $ cuvinte cheie matrice. Acest lucru va permite colaboratorului să sprijine fie: shouldReturn ()ar trebui să fie()shouldEqual () saushouldBeEqualTo (), sau shouldNotReturn ()nu ar trebui să fie()shouldNotEqual () sau shouldNotBeEqualTo ().

De la BasicMatcher, două metode sunt moștenite: positiveMatch () și negativeMatch (). Ele arata astfel:

/ ** * @param string $ nume * @param amestecat $ subiect * @param array $ argumente * * @return amestecat * * @throws FailureException * / funcția publică finală positiveMatch ($ nume, $ subiect, array $ argumente) if false === $ this-> se potrivește ($ subject, $ arguments)) aruncați $ this-> getFailureException ($ name, $ subject, $ arguments);  return $ subject;  

 positiveMatch () metoda face o excepție dacă chibrituri() (metoda abstractă pe care trebuie să o pună în aplicare matricii) se întoarce fals.  negativeMatch () metoda funcționează invers. chibrituri() metoda pentruIdentityMatcher utilizează === operator pentru a compara $ subiect cu argumentul furnizat metodei matcher:

/ ** * @param amestecat $ subject * @param array $ arguments * * @return bool * / meciuri de funcții protejate ($ subject, array $ arguments) return $ subject === $ arguments [0];  

Am putea folosi modulul de identificare astfel:

$ This-> getUser () -> shouldNotBeEqualTo ($ anotherUser); 

Ceea ce s-ar termina în cele din urmă negativeMatch () și asigurați-vă că chibrituri() se întoarce fals.

Uitați-vă la unii dintre ceilalți maeștri și vedeți ce fac ei!

Promisiuni de mai multă magie

Înainte de a termina acest tur scurt al internelor PhpSpec, să aruncăm o privire la încă o bucată de magie:

shouldHaveType ( 'Suhm \ HelloWorld'); var_dump (get_class ($ obiect));  

Prin adăugarea tipului sugerat $ obiect parametru pentru exemplul nostru, PhpSpec va folosi în mod automat reflecția pentru a injecta o instanță a clasei pentru a le folosi. Dar, cu lucrurile pe care le-am văzut deja, avem încredere că avem într-adevăr un exemplu stdClass? Să ne consultăm get_class () încă o dată:

$ vendor / bin / phpspec rulează șirul (28) "PhpSpec \ Wrapper \ Collaborator" 

Nu. In loc de stdClass avem un exemplu de PhpSpec \ Wrapper \ Colaboratorul. Despre ce este vorba?

Ca SubiectColaborator este un înveliș și implementează WrapperInterface. Îl împachetează un exemplu\ Profetia \ Profetia \ ObjectProphecy, care provine din Profeție, cadrul fraier care vine împreună cu PhpSpec. În loc de un stdClass exemplu, PhpSpec ne dă o bănuială. Acest lucru face ca batjocoritorul să fie ușor de făcut cu PhpSpec și ne permite să adăugăm promisiuni obiectelor noastre, cum ar fi:

$ User-> getAge () -> willReturn (10); $ This-> setUser (utilizator $); $ This-> getUserStatus () -> shouldReturn ( 'copil'); 

Cu acest tur scurt al unor părți ale internelor PhpSpec, sper că veți vedea că este mai mult decât un simplu cadru de testare.

Diferența dintre TDD și BDD

PhpSpec este un instrument pentru a face SpecBDD, astfel încât, pentru a obține o mai bună înțelegere, să aruncăm o privire la diferențele dintre dezvoltarea bazată pe test (TDD) și dezvoltarea bazată pe comportament (BDD). După aceea, vom analiza rapid modul în care PhpSpec diferă de alte instrumente, cum ar fi PHPUnit.

TDD este conceptul de a lăsa testele automate să determine proiectarea și implementarea codului. Prin scrierea unor teste mici pentru fiecare caracteristică, înainte de a le implementa efectiv, atunci când obținem un test de trecere, știm că codul nostru satisface acea caracteristică specifică. Cu un test de trecere, după refactorizare, oprim codarea și scriem următorul test. Mantra este "roșu", "verde", "refactor"!

BDD își are originea în - și este foarte asemănător cu - TDD. Sincer, este vorba în principal de o formulare, care este într-adevăr importantă, deoarece poate schimba modul în care ne gândim ca dezvoltatori. În cazul în care TDD vorbește despre testare, BDD vorbește despre descrierea comportamentului. 

Cu TDD ne concentrăm pe verificarea faptului că codul nostru funcționează așa cum ne așteptăm ca acesta să funcționeze, în timp ce cu BDD, ne concentrăm pe verificarea faptului că codul nostru se comportă așa cum ne-o dorim. Un motiv principal pentru apariția BDD, ca alternativă la TDD, este de a evita utilizarea cuvântului "test". Cu BDD nu ne interesează cu adevărat testarea implementării codului nostru, suntem mai interesați să testam ce face (comportamentul său). Când facem BDD, în loc de TDD, avem povești și specificații. Acestea fac scrierea testelor tradiționale redundante.

Povestirile și specificațiile sunt strâns legate de așteptările părților interesate de proiect. Scrierea poveștilor (cu un instrument cum ar fi Behat), se va întâmpla, de preferință, împreună cu părțile interesate sau cu experții domeniului. Povestirile acoperă comportamentul extern. Utilizăm specificațiile pentru a proiecta comportamentul intern necesar pentru a îndeplini pașii poveștilor. Fiecare pas dintr-o poveste poate necesita mai multe iterații cu specificații de scriere și cod de implementare, înainte de a fi satisfăcut. Poveștile noastre, împreună cu specificațiile noastre, ne ajută să ne asigurăm că nu numai că construim un lucru de lucru, ci că este și cel mai bun lucru. Deci, BDD are multe de-a face cu comunicarea.

Cum este PhpSpec diferit de PHPUnit?

Acum câteva luni, un membru notabil al comunității PHP, Mathias Verraes, a postat pe Twitter un "Cadru de testare a unităților într-un tweet". Ideea era să se potrivească codul sursă al unui cadru funcțional de testare a unităților într-un singur tweet. După cum puteți vedea din esență, codul este cu adevărat funcțional și vă permite să scrieți teste unitare de bază. Conceptul de testare a unităților este, de fapt, destul de simplu: verificați un fel de afirmație și notificați utilizatorul cu privire la rezultat.

Desigur, majoritatea cadrelor de testare, cum ar fi PHPUnit, sunt într-adevăr mult mai avansate și pot face mult mai mult decât cadrul lui Mathias, dar încă arată un punct important: Tu afirmi ceva și apoi te desfășoară acel aserțiune pentru tine.

Să aruncăm o privire la un test PHPUnit foarte simplu:

funcția publică testTrue () $ this-> assertTrue (false);  

V-ați putea scrie o implementare super simplă a unui cadru de testare care ar putea executa acest test? Sunt destul de sigur că răspunsul este "da" puteți face asta. La urma urmei, singurul lucru assertTrue () metoda trebuie sa faceti este sa comparati o valoare impotriva Adevărat și aruncă o excepție dacă nu reușește. În centrul său, ceea ce se întâmplă este de fapt destul de drept.

Deci, cum este diferit PhpSpec? În primul rând, PhpSpec nu este un instrument de testare. Testarea codului nu este obiectivul principal al PhpSpec, dar devine un efect secundar dacă îl folosiți pentru a proiecta software-ul prin adăugarea treptată a specificațiilor pentru comportament (BDD). 

În al doilea rând, cred că secțiunile de mai sus ar fi trebuit să clarifice deja modul în care PhpSpec este diferit. Totuși, să comparăm un cod:

// Funcția PhpSpec it_is_initializable () $ this-> shouldHaveType ('Suhm \ HelloWorld');  // Funcția PHPUnit testIsInitializable () $ object = new Suhm \ HelloWorld (); $ this-> assertInstanceOf ('Suhm \ HelloWorld', $ object);  

Deoarece PhpSpec este foarte convingător și face unele afirmații cu privire la modul în care codul nostru este proiectat, ne oferă o modalitate foarte ușoară de a descrie codul nostru. Pe de altă parte, PHPUnit nu face nicio afirmație față de codul nostru și ne permite să facem ceea ce dorim. Practic toate PHPUnit face pentru noi în acest exemplu, este de a rula $ obiect impotrivainstanță de operator. 

Chiar dacă PHPUnit ar putea părea mai ușor să începeți (nu cred că este), dacă nu sunteți atent, puteți cădea cu ușurință în capcane de design rău și de arhitectură, deoarece vă permite să faceți aproape orice. Acestea fiind spuse, PHPUnit poate fi inca mare pentru multe cazuri de utilizare, dar nu este un instrument de proiectare ca PhpSpec. Nu există nici o îndrumare - trebuie să știți ce faceți.

PhpSpec: Un instrument de proiectare

Din site-ul PhpSpec, putem afla că PhpSpec este:

Un set de instrumente php pentru a conduce designul emergent prin specificație.

Permiteți-mi să spun încă o dată: PhpSpec nu este un cadru de testare. Este un instrument de dezvoltare. Un instrument de proiectare software. Nu este un simplu cadru de afirmații care compară valorile și aruncă excepții. Este un instrument care ne ajută în proiectarea și construirea unui cod bine construit. Ne cere să ne gândim la structura codului nostru și să impunem anumite modele arhitecturale, unde o hartă a unei clase corespunde unei singure spec. Dacă întrerupeți principiul responsabilității unice și aveți nevoie să pariați ceva în parte, nu vi se va permite să o faceți.

Spec'ing fericit!

Oh! Și, în sfârșit, deoarece PhpSpec în sine este spec'ed, vă sugerez să mergeți la GitHub și să explorați sursa pentru a afla mai multe.

Cod