Este un adevăr nefericit că, deși principiul de bază din spatele testelor este destul de simplu, introducerea pe deplin a acestui proces în fluxul de lucru de codare de zi cu zi este mai dificilă decât ați putea să sperați. Diferitele jargonuri se pot dovedi copleșitoare! Din fericire, o varietate de instrumente au spatele dvs. și vă ajută să faceți procesul cât mai simplu posibil. Batjocura, cadrul principal de obiecte machete pentru PHP, este un astfel de instrument!
În acest articol, vom săpați în ceea ce este batjocoritor, de ce este util și cum să integrați Mockery în fluxul dvs. de testare.
Un obiect martor nu este altceva decât un jargon de testare care se referă la simularea comportamentului obiectelor reale. În termeni simpli, deseori, atunci când se testează, nu veți dori să executați o anumită metodă. În schimb, pur și simplu trebuie să vă asigurați că a fost, de fapt, chemat.
Poate un exemplu este în ordine. Imaginați-vă că codul dvs. declanșează o metodă care va înregistra un pic de date într-un fișier. Când testați această logică, cu siguranță nu doriți să atingeți fizic sistemul de fișiere. Acest lucru are potențialul de a reduce drastic viteza testelor. În aceste situații, este mai bine să freditați clasa sistemului dvs. de fișiere și, mai degrabă decât să citiți manual fișierul pentru a dovedi că a fost actualizată, pur și simplu asigurați-vă că metoda utilizată în clasă a fost numită. Acest lucru este batjocoritor! Nu e nimic mai mult decât atât; simula comportamentul obiectelor.
Amintiți-vă: jargonul este doar jargon. Nu permiteți niciodată o terminologie confuză pentru a vă descuraja să învățați o nouă abilitate.
În special, pe măsură ce procesul dvs. de dezvoltare se maturizează - inclusiv adoptarea principiului responsabilității unice și folosirea injectării dependenței - o familiaritate cu batjocura devine rapidă.
Mocks vs Stubs: Există șanse mari să audi adesea termenii, a-și bate joc și ciot, aruncate în jurul valorii de interchangable. De fapt, cele două servesc unor scopuri diferite. Primul se referă la procesul de definire a așteptărilor și de asigurare a comportamentului dorit. Cu alte cuvinte, o falsă poate duce la un test eșuat. Un stub, pe de altă parte, este pur și simplu un set fictiv de date care pot fi transferate pentru a îndeplini anumite criterii.
Biblioteca de testare defacto pentru PHP, PHPUnit, navează cu API-ul propriu pentru obiecte bătătorite; totuși, din nefericire, se poate dovedi greu de colaborat. După cum știți cu siguranță, cu cât este mai dificil de testare, cu atât este mai probabil ca dezvoltatorul pur și simplu (și din păcate) să nu.
Din fericire, o varietate de soluții de la terți sunt disponibile prin intermediul ambalajului (depozitul pachetului Composer), care permite o mai mare lizibilitate și, mai important, writeability. Printre aceste soluții - și cel mai notabil dintre seturi - se numără Mockery, un cadru cadru-agnostic de obiecte machete.
Proiectată ca o alternativă pentru cei care sunt copleșiți de verbositatea batjocoritoare a lui PHPUnit, Mockery este o utilitate simplă, dar puternică. Așa cum veți găsi cu siguranță, de fapt, este standardul industriei pentru dezvoltarea PHP modernă.
La fel ca majoritatea instrumentelor PHP în aceste zile, metoda recomandată pentru a instala Mockery este prin Composer (deși este disponibilă și prin Pear).
Stai, ce e chestia asta de compozitor? Este instrumentul preferat al comunității PHP pentru gestionarea dependenței. Acesta oferă o modalitate ușoară de a declara dependențele unui proiect și de a le trage cu o singură comandă. Ca dezvoltator PHP modern, este vital să aveți o înțelegere de bază a ceea ce este compozitorul și cum să îl utilizați.
Dacă lucrați împreună, în scopuri de învățare, adăugați un nou composer.json
fișier într-un proiect gol și adăugați:
"necesită-dev": "batjocură / batjocură": "dev-master"
Acest bit de JSON specifică faptul că, pentru dezvoltare, aplicația dvs. necesită biblioteca Mockery. Din linia de comandă, a compozitor install --dev
va trage în pachet.
$ composer install --dev Încărcarea depozitului compozitorului cu informații despre pachet Instalarea dependențelor (inclusiv necesită-dev) - Instalarea de batjocură / batjocură (dev-master 5a71299) Clonarea 5a712994e1e3ee604b0d355d1af342172c6f475f Fișier de blocare de scriere Generarea fișierelor autoload
Ca un bonus suplimentar, compozitorul are nave cu autoloader proprii gratuit! Fie specificați o clasă de clase de directoare și
compozitor dump-autoload
, sau urmați standardul PSR-0 și ajustați structura directoarelor dvs. pentru a se potrivi. Consultați Nettuts + pentru a afla mai multe. Dacă încă mai solicitați manual fișiere nenumărate în fiecare fișier PHP, ei bine, ați putea face acest lucru greșit.
Înainte de a putea implementa o soluție, este mai bine să examinăm mai întâi problema. Imaginați-vă că trebuie să implementați un sistem de gestionare a procesului de generare a conținutului și de scriere a acestuia într-un fișier. Poate că generatorul compilează diverse date, fie din fișiere locale de fișiere, fie dintr-un serviciu web, iar apoi acele date sunt scrise în sistemul de fișiere.
Dacă respectați principiul responsabilității unice - care dictează că fiecare clasă ar trebui să fie responsabilă pentru un singur lucru - atunci este logic să împărțim această logică în două clase: una pentru generarea conținutului necesar și altul pentru scrierea fizică a datelor într-un fișier. A Generator
și Fişier
clasa, respectiv, ar trebui să facă truc.
Bacsis: De ce nu folosiți
file_put_contents
direct de laGenerator
clasă? Întrebați-vă:Cum aș putea să testez asta??"Există tehnici, cum ar fi patch-uri de maimuțe, care vă permit să supraîncărcați aceste tipuri de lucruri, dar, ca o bună practică, este mai bine să înfășurați astfel de funcționalități, astfel încât să poată fi ușor batjocorit cu unelte,!
Iată o structură de bază (cu o doză sănătoasă de cod pseudo) pentru noi Generator
clasă.
fișier = $ file; funcția protejată getContent () // simplificată pentru a reveni la demo 'foo bar'; funcția publică de funcționare () $ content = $ this-> getContent (); $ this-> file-> put ('foo.txt', $ content);
Acest cod folosește ceea ce numim injecție de dependență. Încă o dată, acesta este pur și simplu jargonul dezvoltatorului pentru injectarea dependențelor unei clase prin metoda constructorului, mai degrabă decât codarea greu a acestora.
De ce este aceasta benefică? Pentru că, altfel, nu am fi putut să ne batem Fişier
clasă! Sigur, am putea să fugim Fişier
clasa, dar dacă instanțierea sa este codificată greu în clasa pe care o testăm, nu există o cale ușoară de înlocuire a acelei instanțe cu versiunea falsă.
funcția publică __construct () // anti-pattern $ this-> file = fișier nou;
Cea mai bună modalitate de a construi o aplicație testabilă este de a aborda fiecare apel de metodă nouă cu întrebarea "Cum aș putea să testez asta??"În timp ce există trucuri pentru a obține în jurul acestei greu-codare, acest lucru este considerat pe scară largă ca fiind o practică proastă. În schimb, întotdeauna injectați dependențele unei clase prin constructor, sau prin injectare setter.
Injectarea injectorului este mai mult sau mai puțin identică cu injecția constructorului. Principiul este exact același; singura diferență constă în faptul că, mai degrabă, prin injectarea dependențelor clasei prin metoda constructorului, ele se fac în schimb printr-o metodă de setare, cum ar fi:
funcția publică setFile (fișier $ file) $ this-> file = $ file;
O critică obișnuită a injectării de dependență este aceea că introduce o complexitate suplimentară într-o aplicație, toate pentru a fi mai testabile. Deși argumentul complexității este discutabil în opinia acestui autor, dacă preferați, puteți permite injecția de dependență, în timp ce încă precizați valorile implicite. Iată un exemplu:
generatorul de clasă funcția publică __construct (fișier $ file = null) $ this-> file = $ file?: fișier nou;
Acum, dacă o instanță Fişier
este trecută către constructor, acel obiect va fi folosit în clasă. Pe de altă parte, dacă nu se va trece nimic, Generator
voi da înapoi să instanțiați manual clasa aplicabilă. Acest lucru permite astfel de variații, cum ar fi:
# Instanțe de clasă Fișier nou Generator; # Injectați fișierul Generator nou (fișier nou); # Injectați o falsă fișier pentru testarea unui nou Generator ($ mockedFile);
Continuând, în scopul acestui tutorial, Fişier
clasa nu va fi nimic mai mult decât o simplă învelire în jurul PHP-ului file_put_contents
funcţie.
Mai degrabă simplu, nu? Să scriem un test pentru a vedea, la prima vedere, care este problema.
foc();Rețineți că aceste exemple presupun că clasele necesare sunt autoloadate cu Compozitor. Ta
composer.json
fișierul acceptă opțional unautoload
obiect, unde puteți specifica care directoare sau clase să se autoloadă. Nu mai murdarnecesita
declaraţii!Dacă lucrați de-a lungul, alergați
PHPUnit
va reveni:OK (1 test, 0 afirmații)Este verde; asta înseamnă că putem trece la următoarea sarcină, nu? Nu exact. Deși este adevărat că codul nu funcționează, de fiecare dată când acest test este rulat, a
Deși testele trec, ele ating în mod incorect sistemul de fișiere.foo.txt
fișierul va fi creat pe sistemul de fișiere. Dar când ai scris mai multe teste? După cum vă puteți imagina, foarte repede, viteza de execuție a testului va stutura.Încă nu sunt convinși? Dacă viteza de testare redusă nu vă va influența, atunci luați în considerare bunul simț. Gândiți-vă: încercăm
Generator
clasă; de ce avem vreun interes în executarea codului dinFişier
clasă? Ar trebui să aibă propriile teste! De ce naștem să ne ducem?
Soluția
Sperăm că secțiunea anterioară a furnizat ilustrația perfectă pentru motivul pentru care batjocura este esențială. După cum sa observat mai devreme, deși am putea folosi API-ul nativ al PHPUnit pentru a servi cerințele noastre de batjocură, nu este deloc plăcut să lucrăm cu el. Pentru a ilustra acest adevăr, iată un exemplu pentru a afirma că un obiect batjocorit ar trebui să primească o metodă,
getName
și întoarcereJohn Doe
.funcția publică funcțiaNativeMocks () $ mock = $ this-> getMock ("SomeClass"); $ mock-> se așteaptă ($ this-> once ()) -> method ('getName') -> va ($ this-> returnValue ('John Doe'));În timp ce își face treaba - afirmând că a
getName
metoda se numește odată și se întoarce John Doe - Implementarea PHPUnit este confuză și verbală. Cu Mockery, putem îmbunătăți drastic lizibilitatea acestuia.funcția publică testMockery () $ mock = Mockery :: mock ("SomeClass"); $ mock-> shouldReceive ('getName') -> o dată () -> șiReturn ("John Doe");Observați cum acest ultim exemplu citește (și vorbește) mai bine.
Continuând cu exemplul din "Dilemă secțiune, de data aceasta, în cadrul
GeneratorTest
clasa, hai să batem în schimb - sau simula comportamentul -Fişier
clasă cu batjocură. Iată codul actualizat:shouldReceive ('put') -> cu ('foo.txt', 'foo bar') -> o dată (); $ generator = generator nou ($ mockedFile); $ Generator-> foc ();Confuzat de către
Batjocorirea :: close ()
referință în cadruldărâma
metodă? Acest apel static curăță containerul Mockery utilizat de testul curent și execută orice sarcină de verificare necesară pentru așteptările dvs..O clasă poate fi batjocorită cu ajutorul lizibilului
Batjocorirea :: bate joc ()
metodă. În continuare, de obicei, trebuie să specificați metodele pe care doriți să le numiți acest obiect mock, împreună cu toate argumentele aplicabile. Acest lucru poate fi realizat, prinshouldReceive (METODA)
șicu (ARG)
metode.În acest caz, când sunăm
$ Generate-> foc ()
, noi afirmam ca ar trebui sa sunea pune
metoda peFişier
instanță și trimiteți calea,foo.txt
, și datele,foo bar
.// biblioteci / Generator.php funcția publică de funcționare () $ content = $ this-> getContent (); $ this-> file-> put ('foo.txt', $ content);Pentru că folosim injectarea de dependență, acum este un cinch pentru a injecta în loc de batjocorit
Fişier
obiect.$ generator = generator nou ($ mockedFile);Dacă vom efectua din nou testele, ei vor reveni în continuare în verde, cu toate acestea
Fişier
clasa - și, prin urmare, sistemul de fișiere - nu va fi niciodată atins! Din nou, nu este nevoie să atingețiFişier
. Ar trebui să aibă propriile teste! Mocking pentru victorie!Simple Mock Object
Obiectele mocking nu necesită întotdeauna o referință la o clasă. Dacă aveți nevoie doar de un obiect simplu, poate pentru un utilizator, puteți trece un tablou la
a-și bate joc
metoda - unde, pentru fiecare element, cheia și valoarea corespund numelui metodei și, respectiv, valorii returnate.funcția publică testSimpleMocks () $ user = Mockery :: mock (['getFullName' => 'Jeffrey Way']); $ User-> getFullName (); // Jeffrey WayValorile returnează din metodele deranjate
Cu siguranta vor exista momente, atunci cand o metoda de clasa batjocorita trebuie sa returneze o valoare. Continuând cu exemplul Generator / Fișier, ce ar fi dacă trebuie să ne asigurăm că, dacă fișierul există deja, nu ar trebui să fie suprascris? Cum putem realiza asta??
Cheia este de a utiliza
andReturn ()
metoda pe obiectul batjocorit pentru a simula diferite statele. Iată un exemplu actualizat:funcția publică testDoesNotOverwriteFile () $ mockedFile = Mockery :: mock ("File"); $ mockedFile-> shouldReceive ('există') -> o dată () -> șiReturn (true); $ mockedFile-> shouldReceive ("pune") -> niciodată (); $ generator = generator nou ($ mockedFile); $ Generator-> foc ();Acest cod actual actualmente afirmă că o
există
metoda ar trebui să fie declanșată asupra batjocorituluiFişier
clasa, și ar trebui, în scopul acestei căi, să revinăAdevărat
, semnalând că fișierul există deja și nu trebuie suprascris. În continuare, ne asigurăm că, în astfel de situații,a pune
metoda peFişier
clasa nu este niciodată declanșată. Cu batjocură, acest lucru este ușor, datoritănu()
așteptare.$ mockedFile-> shouldReceive ("pune") -> niciodată ();Dacă vom efectua din nou testele, va fi returnată o eroare:
Metoda există () din Fișier ar trebui să fie numită exact de 1 ori, dar numită de 0 ori.Aha; așa că testul se aștepta la asta
$ This-> File-> există ()
ar trebui să fie chemat, dar asta nu sa întâmplat niciodată. Ca atare, a eșuat. Să rezolvăm asta!fișier = $ file; funcția protejată getContent () // simplificată pentru a reveni la demo 'foo bar'; funcția publică de funcționare () $ content = $ this-> getContent (); $ fișier = 'foo.txt'; dacă (! $ this-> fișier-> există ($ file)) $ this-> file-> put ($ file, $ content);Cam despre asta e! Nu numai că am urmat un ciclu TDD (test-driven development), dar testele sunt înapoi la verde!
Este important să rețineți că acest stil de testare este eficient numai dacă, de fapt, testați și dependențele clasei dvs.! În caz contrar, deși testele pot apărea verde, pentru producție, codul se va rupe. Demo-ul nostru de până acum ne-a asigurat doar asta
Generator
funcționează conform așteptărilor. Nu uita să testeziFişier
de asemenea!
Așteptări
Să ne sătăm puțin mai adânc în declarațiile așteptărilor lui Mockery. Voi deja sunteți familiarizați
shouldReceive
. Fii atent cu asta; numele său este puțin înșelător. Atunci când este lăsată pe cont propriu, nu este necesar ca metoda să fie declanșată; valoarea implicită este zero sau de mai multe ori (zeroOrMoreTimes ()
). Pentru a afirma că aveți nevoie de o singură dată sau de mai multe ori pentru o metodă, sunt disponibile câteva opțiuni:$ Mock-> shouldReceive ( 'metoda') -> o dată (); $ Mock-> shouldReceive ( 'metoda') -> ori (1); $ Mock-> shouldReceive ( 'metoda') -> ATLEAST () -> ori (1);Vor exista momente în care sunt necesare constrângeri suplimentare. După cum sa demonstrat mai devreme, acest lucru poate fi util în special când trebuie să vă asigurați că o anumită metodă este declanșată cu argumentele necesare. Este important să rețineți că așteptarea se va aplica numai dacă se va apela o metodă cu aceste argumente exacte.
Iată câteva exemple.
$ Mock-> shouldReceive ( 'get') -> withAnyArgs () -> o dată (); // default $ mock-> shouldReceive ('get') -> cu ('foo.txt') -> o dată (); $ mock-> shouldReceive ('pune') -> cu ('foo.txt', 'foo bar') -> o dată ();Aceasta poate fi extinsă și mai mult pentru a permite ca valorile argumentului să fie dinamice în natură, atâta timp cât îndeplinesc anumite criterii. Poate că numai dorim să ne asigurăm că un șir este trecut la o metodă:
$ Mock-> shouldReceive ( 'get') -> cu (tip batjocorirea :: ( 'string')) -> o dată ();Sau poate că argumentul trebuie să se potrivească cu o expresie regulată. Să afirmăm că orice nume de fișier se termină
.txt
ar trebui să fie potrivite.$ mockedFile-> shouldReceive ('pune') -> cu ('/ \. txt $ /', Mockery :: any ()) -> o dată ();Și ca un exemplu final (dar fără a se limita la), să permitem o gamă de valori acceptabile, folosind
oricare dintre
Matcher.$ mockedFile-> shouldReceive ('get') -> cu (Mockery :: anyOf ('log.txt', 'cache.txt')) -> o dată ();Cu acest cod, așteptarea se va aplica numai dacă primul argument pentru
obține
metoda estelog.txt
saucache.txt
. În caz contrar, o excepție de batjocură va fi aruncată atunci când sunt executate testele.Impuscaturi \ Exceptie \ NoMatchingExpectationException: Nu a fost gasit nici un handler potrivit ...Bacsis: Nu uitați, puteți oricând să faceți alias
Bătaie de joc
la fel dem
în partea de sus a clasei tale pentru a face lucrurile un pic mai succint:utilizați batjocorirea ca m;
. Acest lucru permite o descriere mai succintă,m :: mock ()
.În cele din urmă, avem o varietate de opțiuni pentru a specifica ce ar trebui să facă sau să se întoarcă metoda martoră. Poate că avem nevoie doar de returnarea unui boolean. Uşor:
$ mock-> shouldReceive ("metoda") -> o dată () -> șiReturn (false);
Momeală parțială
S-ar putea să constatați că există situații în care trebuie doar să falsificați o singură metodă, mai degrabă decât întregul obiect. Să ne imaginăm, pentru scopurile acestui exemplu, că o metodă din clasa ta face referire la o funcție globală personalizată (gasp) pentru a prelua o valoare dintr-un fișier de configurare.
getOption ( 'timeout'); // face ceva cu $ timeoutDeși există câteva tehnici diferite pentru batjocorirea funcțiilor globale. cu toate acestea, este mai bine să evitați această metodă apelând împreună. Aceasta este tocmai atunci când jocurile parțiale intră în joc.
funcția publică funcționalăPartialMockExample () $ mock = Mockery :: mock ('MyClass [getOption]'); $ mock-> shouldReceive ('getOption') -> o dată () -> șiReturn (10000); $ Mock-> foc ();Observați modul în care am plasat metoda pentru a bate joc în paranteze. Dacă aveți mai multe metode, separați-le cu o virgulă, după cum urmează:
$ mock = Mockery :: mock ('MyClass [metoda1, metoda2]');Cu această tehnică, restul metodelor de pe obiect vor declanșa și se vor comporta așa cum ar face în mod normal. Rețineți că trebuie să declarați întotdeauna comportamentul metodelor batjocoritoare, așa cum am făcut mai sus. În acest caz, când
getOption
este numit, mai degrabă decât executarea codului în ea, ne întoarcem pur și simplu10000
.O alternativă alternativă este să utilizați machete parțiale pasive, pe care le puteți considera ca setând o stare implicită pentru obiectul machet: toate metodele sunt amânate la clasa părinte principală, cu excepția cazului în care se specifică o așteptare.
Fragmentul de cod anterior poate fi rescris ca:
funcția publică funcționalăPassiveMockExample () $ mock = Mockery :: mock ('MyClass') -> makePartial (); $ mock-> shouldReceive ('getOption') -> o dată () -> șiReturn (10000); $ Mock-> foc ();În acest exemplu, toate metodele sunt activate
Clasa mea
se vor comporta așa cum ar face în mod normal, excluzândgetOption
, care va fi batjocorit și va reveni 10000 ".
Hamcrest
Biblioteca Hamcrest oferă un set suplimentar de matematici pentru definirea așteptărilor.Odată ce v-ați familiarizat cu API-ul Mockery, vă recomandăm să folosiți și biblioteca Hamcrest, care oferă un set suplimentar de matrici pentru definirea așteptărilor lizibile. La fel ca batjocura, poate fi instalat prin compozitor.
"requ-dev": "batjocură / batjocură": "dev-master", "davedevelopment / hamcrest-php": "dev-master"Odată instalat, puteți folosi o notație mai ușor de citit de om pentru a defini testele. Mai jos sunt câteva exemple, inclusiv mici variații care au același rezultat final.
Observați cum Hamcrest vă permite să vă scrieți afirmațiile într-o manieră lizibilă sau tersă așa cum doriți. Utilizarea
este()
funcția nu este nimic mai mult decât zahăr sintactic pentru a ajuta la citire.Veți găsi că Mockery se amestecă destul de frumos cu Hamcrest. De exemplu, numai cu Mockery, pentru a specifica că trebuie apelată o metodă martoră cu un singur argument de tip,
şir
, ați putea scrie:$ mock-> shouldReceive ('metoda') -> cu (Mockery :: type ('string')) -> o dată ();Dacă utilizați Hamcrest,
Batjocorirea :: Tip
pot fi înlocuite cuStringValue ()
, ca astfel:$ mock-> shouldReceive ('metoda') -> cu (stringValue ()) -> o dată ();Hamcrest urmărește resursăValoarea convenției de denumire pentru potrivirea tipului unei valori.
nullValue
valoare intreaga
arrayValue
Alternativ, pentru a potrivi orice argument, Batjocorirea :: orice ()
poate deveni orice()
.
$ file-> shouldReceive ('pune') -> cu ('foo.txt', anything ()) -> once ();
Cel mai mare obstacol în calea folosirii batjocoritorului este, în mod ironic, nu API, în sine.
Cel mai mare obstacol în calea folosirii de batjocură este, în mod ironic, nu API-ul în sine, ci înțelegerea motivului și a momentului în care să folosiți machete în testarea dvs..
Cheia este de a învăța și respecta principiul responsabilității unice în fluxul de lucru de codare. Început de Bob Martin, SRP dictează că o clasă "ar trebui să aibă unul, și unul singur, un motiv să se schimbe."Cu alte cuvinte, o clasă nu ar trebui să fie actualizată ca răspuns la mai multe modificări independente ale aplicației dvs., cum ar fi modificarea logicii de afaceri sau modul în care este formatată ieșirea sau modul în care pot fi persistente datele. ca o metodă, o clasă ar trebui să facă un lucru.
Fişier
clasa gestionează interacțiunile sistemului de fișiere. A MysqlDb
repository persistă date. Un E-mail
clasa pregătește și trimite e-mailuri. Observați cum, în nici unul dintre aceste exemple nu a fost cuvântul, și, folosit.
Odată ce acest lucru este înțeles, testarea devine mult mai ușoară. Injecția de dependență ar trebui utilizată pentru toate operațiile care nu se încadrează în clasa umbrelă. Când se testează, se concentrează asupra unei clase la un moment dat și se bat joc de toate dependențele sale. Nu sunteți interesat să le testați oricum; au propriile teste!
Deși nimic nu vă împiedică să utilizați implementarea nașterii bâlbâitoare a PHPUnit, de ce vă deranjez când citirea mai bună a lui Mockery este doar o compozitor
departe?