Î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.
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.
Î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.
Pentru acest tutorial, presupun că aveți următoarele lucruri susținute:
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
Î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.
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.
$ 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.
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.
Î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 onumara()
metodă.Mai devreme, am folosit dispozitivul de compensare
shouldHaveType ()
, care este un tip matcher. Utilizează comparatorul PHPinstanță 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 esteshouldImplement ()
, 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 nostruTaskCollection
clasă:clasa TaskCollection implementează \ CountableTestele 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
, nu0
. 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 acesteaColaborator
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? yAdă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ătreadă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 faptadă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ămsarcini $
proprietate peTodoList
. În cele din urmă, încercăm să sunăm de faptaddTask ()
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ăratnumara()
metodă. Trebuie doar să fim siguri că codul nostru sună la cevanumara()
ș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ătitsarcini $
proprietatea asupra obiectului nostru. În cele din urmă, încercăm să apelăm o metodă,hasTasks ()
, și asigurați-vă că se întoarcefals
.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 întoarcenul
, nufals
.Î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 esteAdevărat
, așa că trebuie să ne îmbunătățim codul. Să folosim astanumara()
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 mostenesteObjectBehavior
. 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 msAm 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!