Noțiuni de bază cu Phpspec

În acest tutorial scurt, dar cuprinzător, vom analiza dezvoltarea bazată pe comportament (BDD) cu phpspec. În cea mai mare parte, va fi o introducere la instrumentul phpspec, dar pe măsură ce mergem, vom atinge diferite concepte BDD. BDD este un subiect fierbinte în aceste zile și phpspec a câștigat multă atenție în comunitatea PHP recent.

SpecBDD & Phpspec

BDD se referă la descrierea comportamentului software-ului, pentru a obține dreptul de design. Acesta este adesea asociat cu TDD, dar în timp ce TDD se concentrează asupra testarea cererea dvs., BDD este mai mult despre descriind comportamentul acestuia. Utilizarea unei abordări BDD vă va forța să luați în considerare în mod constant cerințele și comportamentul dorit al software-ului pe care îl construiți.

Două instrumente BDD au câștigat foarte multă atenție în comunitatea PHP recent, Behat și phpspec. Behat vă ajută să descrieți comportamentul extern din aplicația dvs., folosind limba limbajului citit. phpspec, pe de altă parte, vă ajută să descrieți comportamentul intern din aplicația dvs., scriind mici "specificații" în limba PHP - de aici SpecBDD. Aceste specificații testează faptul că codul dvs. are comportamentul dorit.

Ce vom face

În acest tutorial, vom acoperi tot ce are legătură cu a începe cu phpspec. În drumul nostru, vom construi fundamentul unei aplicații listă todo, pas cu pas, folosind o abordare SpecBDD. În timp ce mergem, vom avea phpspec să vedem drumul!

Notă: Acesta este un articol intermediar despre PHP. Presupun că ai o bună înțelegere a PHP-ului orientat pe obiect.

Instalare

Pentru acest tutorial, presupun că aveți următoarele lucruri susținute:

  • O configurare de lucru PHP (min 5.3)
  • Compozitor

Instalarea phpspec prin Composer este cea mai ușoară cale. Tot ce trebuie să faceți este să executați următoarea comandă într-un terminal:

$ complier necesită phpspec / phpspec Furnizați o constrângere de versiune pentru cerința phpspec / phpspec: 2.0.*@dev

Acest lucru va face a composer.json fișier pentru tine și instala phpspec într-o furnizor / director.

Pentru a vă asigura că totul funcționează, rulați phpspec și vedeți că obțineți următoarea ieșire:

$ vendor / bin / phpspec execută 0 specificații 0 exemple 0ms

configurație

Înainte de a începe, trebuie să facem puțină configurație. Când rulează phpspec, acesta caută un fișier YAML numit phpspec.yml. Deoarece vom introduce codul nostru într-un spațiu de nume, trebuie să ne asigurăm că phpspec știe acest lucru. De asemenea, în timp ce suntem la aceasta, să ne asigurăm că specificațiile noastre arată frumos și frumos atunci când le executăm.

Continuați și creați fișierul cu următorul conținut:

formatter.name: destul de suite: todo_suite: namespace: Petersuhm \ Todo

Există multe alte opțiuni de configurare disponibile, despre care puteți citi în documentație.

Un alt lucru pe care trebuie să-l facem este să-i spunem compozitorului cum să ne autolizeze codul. phpspec va folosi autoloaderul Compozitorului, deci este necesar ca specificațiile noastre să fie difuzate.

Adăugați un element autoload la composer.json fișier pe care Composerul la făcut pentru dvs.:

"necesită": "phpspec / phpspec": "2.0.*@dev", "autoload": "psr-0": "Petersuhm \\ Todo": "src"

Alergare compozitor dump-autoload va actualiza autoloader-ul după această modificare.

Prima noastra spec

Acum suntem gata să scriem primul nostru spec. Vom începe prin a descrie o clasă numită TaskCollection. Vom avea phpspec să genereze o clasă spec pentru noi folosind descrie comandă (sau, alternativ, versiunea scurtă desc) .

$ vendor / bin / phpspec descrie "Petersuhm \ Todo \ TaskCollection" $ vendor / bin / phpspec run Doriți să vă creez "Petersuhm \ Todo \ TaskCollection" pentru tine? y

Deci, ce sa întâmplat aici? În primul rând, am solicitat phpspec să creeze o spec TaskCollection. În al doilea rând, am rulat suita noastră spec și apoi phpspec automagic oferit să creeze realitatea TaskCollection clasa pentru noi. Răcoros, nu-i așa??

Continuați și rulați din nou suita și veți vedea că deja avem un exemplu în spec. (Vom vedea într-un moment ce este un exemplu):

$ vendor / bin / phpspec execută Petersuhm \ Todo \ TaskCollection 10 ✔ este inițializabil 1 specificații 1 exemplar (1 trecut) 7ms

Din această ieșire putem vedea că TaskCollection este inițializabil. Despre ce este vorba? Aruncați o privire la fișierul spec. Generat de phpspec și ar trebui să fie mai clar:

shouldHaveType ( 'Petersuhm \ Todo \ TaskCollection'); 

Fraza "este inițializabilă" derivă dintr-o funcție numită it_is_initializable () care phpspec a adăugat la o clasă numită TaskCollectionSpec. Această funcție este ceea ce numim noi ca fiind exemplu. În acest exemplu particular, avem ceea ce numim a Matcher denumit shouldHaveType () care verifică tipul nostru TaskCollection. Dacă schimbați parametrul trecut la această funcție la altceva și rulați din nou spec., Veți vedea că acesta va eșua. Înainte de a înțelege complet acest lucru, cred că trebuie să cercetăm în ce variabilă $ this se referă la spec.

Ce este $ this?

Desigur, $ this se referă la instanța clasei TaskCollectionSpec, deoarece acesta este doar un cod PHP regulat. Dar cu phpspec trebuie să te descurci $ this diferit de ceea ce faci în mod normal, deoarece sub capotă se referă efectiv la obiectul testat, care este de fapt TaskCollection clasă. Acest comportament este moștenit din clasă ObjectBehavior, care vă asigură că apelurile pentru funcții sunt direcționate către clasa specializată. Aceasta înseamnă că SomeClassSpec se va apela metoda proxy la o instanță de SomeClass. phpspec va înfășura aceste apeluri de metodă pentru a rula valorile lor de întoarcere față de utilizatorii de genul celui pe care tocmai l-ați văzut.

Nu aveți nevoie de o înțelegere profundă a acestui lucru pentru a utiliza phpspec, doar amintiți-vă că, în ceea ce vă privește, $ this se referă de fapt la obiectul testat.

Construirea colecției noastre de activități

Până în prezent, nu am făcut nimic pe cont propriu. Dar phpspec a făcut un gol TaskCollection clasa pentru noi de a utiliza. Acum este momentul să completați un cod și să faceți această clasă utilă. Vom adăuga două metode: an adăuga() metoda, adăugarea de sarcini și a numara() , pentru a număra numărul de sarcini din colecție.

Adăugarea unei activități

Înainte de a scrie orice cod real, ar trebui să scriem un exemplu în spec. În exemplul nostru, dorim să încercăm să adăugăm o sarcină colecției și apoi să ne asigurăm că sarcina este, de fapt, adăugată. Pentru a face acest lucru, avem nevoie de o instanță a (până acum nu mai există) Sarcină clasă. Dacă adăugăm această dependență ca parametru pentru funcția noastră spec, phpspec ne va da în mod automat o instanță pe care o putem folosi. De fapt, instanța nu este o instanță reală, dar la ce se referă phpspec ca a Colaborator. Acest obiect va acționa ca obiect real, însă phpspec ne permite să facem mai multe lucruri cu acest lucru, pe care le vom vedea în curând. Chiar dacă Sarcină clasa nu există încă, deocamdată, doar să pretind că o face. Deschideți TaskCollectionSpec și adăugați a utilizare declarație pentru Sarcină și apoi adăugați exemplul it_adds_a_task_to_the_collection ():

utilizați Petersuhm \ Todo \ Task; ... funcția it_adds_a_task_to_the_collection (sarcina $ task) $ this-> add ($ task); $ This-> sarcini [0] -> shouldBe ($ sarcină); 

În exemplul nostru, scriem codul "dorim să avem". Noi numim adăuga() metodă și apoi încercați să o dați $ sarcină. Apoi verificăm dacă sarcina a fost adăugată la variabila instanței sarcini $. Măcelarul ar trebui să fie() este un identitate matcher similar cu PHP === comparator. Puteți folosi oricare dintre ele ar trebui să fie(), shouldBeEqualTo (), shouldEqual () sau shouldReturn () - toți fac același lucru.

Rularea phpspec va produce unele erori, deoarece nu avem o clasă numită Sarcină inca.

Hai să rezolvăm phpspec asta pentru noi:

$ vendor / bin / phpspec descrie "Petersuhm \ Todo \ Task" $ vendor / bin / phpspec run Doriți să vă creez "Petersuhm \ Todo \ Task" pentru tine? y

Făcând din nou phpspec, ceva interesant se întâmplă:

$ vendor / bin / phpspec run Doriți să vă creez 'Petersuhm \ Todo \ TaskCollection :: add ()' pentru tine? y

Perfect! Dacă te uiți la TaskCollection.php fișier, veți vedea că phpspec a făcut o adăuga() functie pentru a completa:

phpspec se plânge în continuare. Nu avem sarcini $ array, deci hai să facem una și să adăugăm sarcina la ea:

sarcini [] = $ task; 

Acum specificatiile noastre sunt frumoase si verzi. Rețineți că m-am asigurat să tastați $ sarcină parametru.

Doar pentru a ne asigura că am reușit, să adăugăm o altă sarcină:

funcția it_adds_a_task_to_the_collection (Task $ task, Task $ anotherTask) $ this-> add ($ task); $ This-> sarcini [0] -> shouldBe ($ sarcină); $ This-> adaugă ($ anotherTask); $ This-> sarcini [1] -> shouldBe ($ anotherTask); 

Făcând phpspec, se pare că suntem cu toții buni.

Implementarea sistemului Numărabile Interfață

Vrem să știm câte sarcini sunt într-o colecție, ceea ce reprezintă un motiv foarte important pentru utilizarea uneia dintre interfețele din Biblioteca Standardă de PHP (SPL), și anume Numărabile interfață. Această interfață impune o clasă care o implementează trebuie sa ia o numara() metodă.

Mai devreme, am folosit dispozitivul de compensare shouldHaveType (), care este un tip matcher. Utilizează comparatorul PHP instanță de pentru a valida că un obiect este, de fapt, o instanță a unei clase date. Există 4 categorii de prieteni, care toți fac același lucru. Unul dintre ei este shouldImplement (), care este perfect pentru scopul nostru, așa că haideți să mergem mai departe și să folosim asta într-un exemplu:

funcția it_is_countable () $ this-> shouldImplement ('Countable'); 

Vezi cât de frumoasă citește? Să conducem exemplul și să avem phpspec să ne conducă calea:

$ vendor / bin / phpspec rula Petersuhm / Todo / TaskCollection 25 ✘ este numită așteptată o instanță de Countable, dar a primit [obj: Petersuhm \ Todo \ TaskCollection].

Bine, deci clasa noastră nu este o instanță Numărabile deoarece nu am implementat-o ​​încă. Să actualizăm codul nostru TaskCollection clasă:

clasa TaskCollection implementează \ Countable

Testele noastre nu vor fi difuzate, deoarece Numărabile interfața are o metodă abstractă, numara(), pe care trebuie să o punem în aplicare. O metodă goală va face truc pentru moment:

funcția publică () // ...

Și ne întoarcem la verde. În momentul de față numara() metoda nu face prea mult, și de fapt este destul de inutil. Să scriem un spec pentru comportamentul pe care dorim să-l aibă. În primul rând, fără sarcini, este de așteptat ca numărătoarea noastră să revină la zero:

funcția it_counts_elements_of_the_collection () $ this-> count () -> shouldReturn (0); 

Se întoarce nul, nu 0. Pentru a obține un test verde, să rezolvăm acest lucru modul TDD / BDD:

funcția publică () return 0; 

Suntem verzi și totul este bun, dar probabil nu este vorba de comportamentul pe care îl dorim. În schimb, să ne extindem spec. Și să adăugăm ceva la sarcini $ matrice:

funcția it_counts_elements_of_the_collection () $ this-> count () -> shouldReturn (0); $ this-> tasks = ['foo']; $ This-> count () -> shouldReturn (1); 

Desigur, codul nostru se mai întoarce 0, și avem un pas roșu. Fixarea acestui lucru nu este prea dificilă și a noastră TaskCollection clasa ar trebui să arate astfel:

sarcini [] = $ task;  count public function () retur count ($ this-> tasks); 

Avem un test verde și al nostru numara() metoda de lucru. Ce mai zi!

Așteptări și promisiuni

Amintiți-vă că v-am spus că phpspec vă permite să faceți lucruri interesante cu exemple de Colaborator clasa, AKA instanțele care sunt injectate automat de phpspec? Dacă ați mai scris teste de unitate înainte, știi ce sunt bâlbâieri. Dacă nu, nu vă îngrijorați prea mult. Este doar jargon. Aceste lucruri se referă la obiecte "false" care vor acționa ca obiectele voastre reale, dar vă vor permite să testați izolați. phpspec va transforma automat acestea Colaborator in situatii in batjocuri si pumnii daca aveti nevoie de ele in specificatiile dumneavoastra. 

Acest lucru este cu adevărat minunat. Sub capotă, phpspec folosește biblioteca Prophecy, care este un cadru extrem de controversat, care joacă bine cu phpspec (și este construit de aceiași oameni minunați). Puteți să așteptați un colaborator (batjocoritor), cum ar fi "această metodă ar trebui să fie chemat", și puteți adăuga promisiuni (stubbing), cum ar fi" această metodă va reveni această valoare ". Cu phpspec acest lucru este foarte ușor și vom face ambele lucruri în continuare.

Să facem o clasă, o să-i spunem TodoList, care pot face uz de clasa noastră de colectare.

$ vendor / bin / phpspec desc "Petersuhm \ Todo \ TodoList" $ vendor / bin / phpspec run Doriți să vă creez "Petersuhm \ Todo \ TodoList" pentru tine? y

Adăugarea de sarcini

Primul exemplu pe care îl vom adăuga este unul pentru adăugarea de sarcini. Vom face o addTask () care nu face altceva decât să adauge o sarcină colecției noastre. Pur și simplu direcționează apelul către adăuga() metoda de colectare, deci acesta este un loc perfect pentru a face uz de o așteptare. Nu vrem ca metoda să sune de fapt adăuga() metoda, vrem doar să ne asigurăm că încearcă să o facă. În plus, vrem să ne asigurăm că o numește doar o singură dată. Aruncati o privire la modul in care putem face acest lucru cu phpspec:

shouldHaveType ( 'Petersuhm \ Todo \ TodoList');  funcția it_adds_a_task_to_the_list (TaskCollection $ tasks, Task $ task) $ tasks-> add ($ task) -> shouldBeCalledTimes (1); $ this-> tasks = $ tasks; $ This-> addTask ($ sarcină); 

În primul rând, ne-am oferit phpspec cu cei doi colaboratori de care avem nevoie: o colecție de sarcini și o sarcină. Apoi ne-am pus o așteptare pe colaboratorul colecției de sarcini care spune în esență: " adăuga() metoda ar trebui să fie numită exact 1 dată cu variabila $ sarcină ca parametru. "Așa ne pregătim pe colaboratorul nostru, care este acum un bătăuș, înainte să îl alocăm sarcini $ proprietate pe TodoList. În cele din urmă, încercăm să sunăm de fapt addTask () metodă.

Ok, ce are de spus phpspec despre acest lucru:

$ vendor / bin / phpspec executați Petersuhm / Todo / TodoList 17! adaugă o sarcină la lista proprietăților listă care nu au fost găsite.

sarcini $ proprietatea nu există - una ușoară:

Încearcă din nou și ai ghidul phpspec în felul următor:

$ vendor / bin / phpspec run Doriți să vă creez 'Petersuhm \ Todo \ TodoList :: addTask ()' pentru tine? y $ vendor / bin / phpspec rula Petersuhm / Todo / TodoList 17 ✘ adaugă o sarcină la listă, unele predicții au eșuat: Dublă \ Petersuhm \ Todo \ TaskCollection \ P4: P4-> adăugați (exact (Double \ Petersuhm \ Todo \ Task \ P3: 000000002544d76d0000000059fcae53)) dar nu s-au făcut.

Bine, acum sa întâmplat ceva interesant. Consultați mesajul "S-a așteptat exact 1 apeluri care se potrivesc: ..."? Aceasta este așteptarea noastră nereușită. Acest lucru se întâmplă deoarece după ce apelați addTask () metodă, adăuga() metoda de colectare a fost nu numită, pe care am așteptat-o ​​să fie.

Pentru a reveni la verde, completați următorul cod în spațiul gol addTask () metodă:

tasks-> adaugă (sarcină $); 

Înapoi la verde! Se simte bine, corect?

Verificarea sarcinilor

Să aruncăm o privire și la promisiuni. Vrem o metodă care să ne poată spune dacă există sarcini în colecție. Pentru aceasta, vom verifica pur și simplu valoarea returnată a numara() privind colectarea. Din nou, nu avem nevoie de o instanță reală cu adevărat numara() metodă. Trebuie doar să fim siguri că codul nostru sună la ceva numara() și faceți unele lucruri în funcție de valoarea returnată.

Aruncați o privire la următorul exemplu:

funcția it_checks_whether_it_has_any_tasks (sarcini TaskCollection $) $ tasks-> count () -> willReturn (0); $ this-> tasks = $ tasks; $ This-> hasTasks () -> shouldReturn (false); 

Avem un colaborator de colectare a sarcinilor care are o funcție numara() această metodă va reveni zero. Aceasta este promisiunea noastră. Ce înseamnă asta este faptul că de fiecare dată când cineva sună numara() metoda, va reveni la zero. Apoi îi atribuim colaboratorului pregătit sarcini $ proprietatea asupra obiectului nostru. În cele din urmă, încercăm să apelăm o metodă, hasTasks (), și asigurați-vă că se întoarce fals.

Ce trebuie să spună phppec despre asta?

$ vendor / bin / phpspec run Doriți să vă creez 'Petersuhm \ Todo \ TodoList :: hasTasks ()' pentru tine? y $ vendor / bin / phpspec rula Petersuhm / Todo / TodoList 25 ✘ verifică dacă are sarcini așteptate false, dar a fost nulă.

Rece. phpspec ne-a făcut a hasTasks () metodă și, în mod surprinzător, se întoarce nul, nu fals.

Încă o dată, aceasta este ușor de rezolvat:

funcția publică hasTasks () return false; 

Ne întoarcem la verde, dar nu este exact ceea ce vrem. Să verificăm dacă există 20 de sarcini. Acest lucru ar trebui să revină Adevărat:

funcția it_checks_whether_it_has_any_tasks (sarcini TaskCollection $) $ tasks-> count () -> willReturn (0); $ this-> tasks = $ tasks; $ This-> hasTasks () -> shouldReturn (false); $ Tasks-> count () -> willReturn (20); $ this-> tasks = $ tasks; $ This-> hasTasks () -> shouldReturn (true); 

Rulați phspec și vom obține:

$ vendor / bin / phpspec rula Petersuhm / Todo / TodoList 25 ✘ verifică dacă are sarcini așteptate, dar a primit false.

bine, fals nu este Adevărat, așa că trebuie să ne îmbunătățim codul. Să folosim asta numara() pentru a vedea dacă există sarcini sau nu:

funcția publică areTasks () if ($ this-> tasks-> count ()> 0) return true; return false; 

Tah dah! Înapoi la verde!

Construirea Matricilor personalizate

O parte din scrierea unor specificații bune este de a le face cât mai ușor de citit. Ultimul nostru exemplu poate fi îmbunătățit într-un mod mic, datorită personajelor personalizate ale phpspec. Este ușor de implementat metode personalizate personalizate - tot ce trebuie să facem este să suprascrii getMatchers () de la care se mosteneste ObjectBehavior. Prin implementarea a două personalizate personalizate, spec. Noastre pot fi modificate pentru a arăta astfel:

funcția it_checks_whether_it_has_any_tasks (sarcini TaskCollection $) $ tasks-> count () -> willReturn (0); $ this-> tasks = $ tasks; $ This-> hasTasks () -> shouldBeFalse (); $ Tasks-> count () -> willReturn (20); $ this-> tasks = $ tasks; $ This-> hasTasks () -> shouldBeTrue ();  funcția getMatchers () return ['beTrue' => funcție ($ subiect) return $ subject === true; , 'beFalse' => funcția (subiectul $) return $ subject === false; ,]; 

Cred că asta arată destul de bine. Rețineți că refacerea specificațiilor dvs. este importantă pentru a le ține la curent. Implementarea propriilor colaboratori personalizați vă poate curăța specificațiile și le va face mai ușor de citit.

De fapt, putem folosi și negarea matematicienilor:

funcția it_checks_whether_it_has_any_tasks (sarcini TaskCollection $) $ tasks-> count () -> willReturn (0); $ this-> tasks = $ tasks; $ This-> hasTasks () -> shouldNotBeTrue (); $ Tasks-> count () -> willReturn (20); $ this-> tasks = $ tasks; $ This-> hasTasks () -> shouldNotBeFalse (); 

Da. Destul de la moda!

Concluzie

Toate specificațiile noastre sunt verzi și ne uităm la cât de frumos ne documentează codul!

 Petersuhm \ Todo \ TaskCollection 10 ✔ este inițializabil 15 ✔ adaugă o sarcină colecției 24 ✔ este numărare 29 ✔ numără elementele colecției Petersuhm \ Todo \ Task 10 ✔ este inițializabil Petersuhm \ Todo \ TodoList 11 ✔ este inițializabil 16 ✔ adaugă o sarcină la listă 24 ✔ verifică dacă are sarcini 3 exemple 8 exemple (8 au trecut) 16 ms

Am descris și am realizat în mod eficient comportamentul dorit al codului nostru. Sa nu mai vorbim, codul nostru este 100% acoperit de specificatiile noastre, ceea ce inseamna ca refactorizarea nu va fi o experienta care sa induca teama.

Urmând, sperăm că te-ai inspirat să dai o încercare de la phpspec. Este mai mult decât un instrument de testare - este un instrument de proiectare. Odată ce vă obișnuiți să utilizați phpspec (și instrumentele de generare a codului minunat), veți avea dificultăți să renunțați la ea din nou! Oamenii se plâng de multe ori că a face TDD sau BDD le încetinește. După ce am încorporat phpspec în fluxul de lucru, simt într-adevăr modul opus - productivitatea mea este îmbunătățită semnificativ. Și codul meu este mai solid!

Cod