Totul despre Mocking cu PHPUnit

Există două stiluri de testare: stilurile "cutie neagră" și "cutie albă". Testul cutie neagră se concentrează asupra stării obiectului; în timp ce testarea casetei albe se concentrează pe comportament. Cele două stiluri se completează reciproc și pot fi combinate pentru a testa foarte bine codul. Batjocoritor ne permite să testați comportamentul și acest tutorial combină conceptul de batjocură cu TDD pentru a construi o clasă de exemplu care folosește mai multe componente pentru a-și atinge scopul.


Pasul 1: Introducere în testarea comportamentului

Obiectele sunt entități care trimit mesaje reciproc. Fiecare obiect recunoaște un set de mesaje la care răspunde la rândul său. Acestea sunt public metode pe un obiect. Privat metodele sunt exact opusul. Ele sunt complet interne unui obiect și nu pot comunica cu nimic în afara obiectului. Dacă metodele publice sunt similare cu mesajele, atunci metodele private sunt similare cu cele ale gândurilor.

Totalul tuturor metodelor publice și private, accesibile prin metode publice, reprezintă comportamentul unui obiect. De exemplu, spuneți unui obiect mișcare determină ca obiectul să nu interacționeze numai cu metodele interne, ci și cu alte obiecte. Din punctul de vedere al utilizatorului, obiectul are doar un comportament simplu: acesta mișcări.

Din punctul de vedere al programatorului, totuși, obiectul trebuie să facă multe lucruri mici pentru a realiza mișcarea.

De exemplu, imaginați-vă că obiectul nostru este o mașină. Pentru ca mișcare, trebuie să aibă un motor în funcțiune, să fie în prima treaptă (sau inversă), iar roțile trebuie să se rotească. Acesta este un comportament pe care trebuie să-l testăm și să-l construim pentru a proiecta și scrie codul nostru de producție.


Pasul 2: Mașină de jucărie controlată la distanță

Clasa noastră testată nu folosește niciodată aceste obiecte falsificate.

Să ne imaginăm că construim un program pentru a controla de la distanță o mașină de jucărie. Toate comenzile pentru clasa noastră vin prin telecomandă. Trebuie să creăm o clasă înțelege ceea ce telecomanda trimite și emite comenzi la masina.

Aceasta va fi o aplicație de exerciții și vom presupune că celelalte clase care controlează diferitele părți ale mașinii sunt deja scrise. Știm semnătura exactă a tuturor acestor clase, însă, din păcate, producătorul de mașini nu ne-a putut trimite un prototip - nici măcar codul sursă. Tot ce știm sunt numele claselor, metodele pe care le au și ce comportament încorporează fiecare metodă. Valorile returnate sunt, de asemenea, specificate.


Pasul 3: Schema de aplicare

Iată schema completă a aplicației. Nu există nicio explicație în acest moment; pur și simplu ține minte pentru o referință ulterioară.


Pasul 4: Încercați dublurile

Un test Stub este un obiect pentru a controla intrarea indirectă a codului testat.

Mocking este un stil de testare care necesită un set propriu de instrumente, un set de obiecte speciale reprezentând niveluri diferite de falsificare a comportamentului obiectului. Acestea sunt:

  • obiecte false
  • testele de încercare
  • spioni de încercare
  • tentative de încercare
  • falsuri de test

Fiecare dintre aceste obiecte are obiectul și comportamentul special. În PHPUnit, ele sunt create cu $ This-> getMock () metodă. Diferența este cum și din ce motive sunt folosite obiectele.

Pentru a înțelege mai bine aceste obiecte, voi pune în aplicare pas cu pas modulul "Toy Car Controller" folosind tipurile de obiecte, în ordine, așa cum sunt enumerate mai sus. Fiecare obiect din listă este mai complex decât obiectul din fața lui. Aceasta duce la o implementare radical diferită de cea din lumea reală. De asemenea, fiind o aplicație imaginară, voi folosi câteva scenarii care ar putea să nu fie fezabile nici într-o mașină adevărată. Dar, hei, să ne imaginăm ce avem nevoie pentru a înțelege imaginea mai largă.


Pasul 5: Obiectul Dummy

Obiectele false sunt obiecte pe care depinde Testul de sistem (SUT), dar ele nu sunt folosite niciodată. Un obiect dummy poate fi un argument transmis unui alt obiect sau poate fi returnat de un al doilea obiect și apoi trimis unui al treilea obiect. Ideea este că clasa noastră testată nu folosește niciodată aceste obiecte falsificate. În același timp, obiectul trebuie să semene cu un obiect real; în caz contrar, receptorul îl poate refuza.

Cea mai bună modalitate de a exemplifica acest lucru este să vă imaginați un scenariu; a cărui schemă este mai jos:

Obiectul portocaliu este RemoteControlTranslator. Scopul principal este să recepționați semnale de la telecomandă și să le traduceți în mesaje pentru clasele noastre. La un moment dat, utilizatorul va face a "Gata de plecare" acționarea de pe telecomandă. Traducătorul va primi mesajul și va crea clasele necesare pentru ca mașina să fie pregătită să meargă.

Producătorul a spus asta "Gata de plecare" înseamnă că motorul este pornit, cutia de viteze este în poziția neutră și lumina este activată sau dezactivată conform solicitării utilizatorului.

Aceasta înseamnă că utilizatorul poate predefini starea luminilor înainte de a fi gata să meargă și se va activa sau dezactiva pe baza acestei valori predefinite la activare. RemoteControlTranslator apoi trimite toate informațiile necesare CarControl clasă' getReadyToGo ($ motor, $ cutie de viteze, $ electronică, $ lumini) metodă. Știu că acest lucru este departe de un design perfect și încalcă câteva principii și modele, dar este foarte bine pentru acest exemplu.

Începeți proiectul cu această structură de fișier inițială:

Amintiți-vă, toate clasele din CarInterface dosarul este furnizat de producătorul mașinii; nu cunoaștem implementarea lor. Tot ce știm sunt semnăturile de clasă, dar nu le pasă de ele în acest moment.

Scopul nostru principal este de a implementa CarController clasă. Pentru a testa această clasă, trebuie să ne imaginăm cum vrem să o folosim. Cu alte cuvinte, ne-am pus pe noi în pantofi RemoteControlTranslator și / sau orice altă clasă viitoare care poate fi utilizată CarController. Să începem prin crearea unui caz pentru clasa noastră.

clasa CarControllerTest extinde PHPUnit_Framework_TestCase 

Apoi adăugați o metodă de testare.

 function testItCanGetReadyTheCar () 

Acum gândiți-vă la ceea ce trebuie să trecem la getReadyToGo () : un motor, o cutie de viteze, un controler de electronice și informații despre lumină. De dragul acestui exemplu, vom bate doar luminile:

requ_once '... /CarController.php'; include "... / autoloadCarInterfaces.php"; clasa CarControllerTest extinde PHPUnit_Framework_TestCase function testItCanGetReadyTheCar () $ carController = nou CarController (); $ engine = motor nou (); $ gearbox = noua cutie de viteze (); $ electornicii = noi Electronice (); $ dummyLights = $ this-> getMock ("Lumini"); $ this-> assertTrue ($ carController-> getReadyToGo ($ motor, $ cutie de viteze, $ electornics, $ dummyLights)); 

Acest lucru se va întâmpla în mod evident cu:

PHP Eroare fatală: Apel la metoda nedefinită CarController :: getReadyToGo ()

În ciuda eșecului, acest test ne-a dat un punct de plecare pentru noi CarController punerea în aplicare. Am inclus un fișier, numit autoloadCarInterfaces.php, care nu era pe lista inițială. Am realizat că am nevoie de ceva pentru a încărca clasele și am scris o soluție foarte fundamentală. Putem întotdeauna să o rescriim când sunt oferite clasele reale, dar aceasta este o poveste cu totul diferită. Pentru moment, vom rămâne cu soluția ușoară:

foreach (scandir (dirname (__FILE__)) / CarInterface) ca nume de fișier $) $ path = dirname (__FILE__). '/ CarInterface /'. nume de fișier $; dacă (is_file ($ path)) necesită calea $ require_once; 

Presupun că acest încărcător de clasă este evident pentru toată lumea; deci, să discutăm codul de testare.

Mai întâi, vom crea o instanță de CarController, clasa pe care dorim să o testăm. Apoi, creăm instanțe ale tuturor celorlalte clase care ne interesează: motor, cutie de viteze și electronică.

Apoi creăm un manechin Lumini obiect apelând la PHPUnit getMock () și transmiterea numelui Lumini clasă. Aceasta returnează o instanță de Lumini, dar fiecare metoda revine nul--un obiect dummy. Acest obiect dummy nu poate face nimic, dar oferă codului nostru interfața necesară pentru a lucra cu Ușoară obiecte.

Este foarte important să observăm acest lucru $ dummyLights este a Lumini obiect și orice utilizator care așteaptă a Ușoară obiect poate folosi obiectul dummy fără să știe că nu este real Lumini obiect.

Pentru a evita confuzia, vă recomandăm să specificați tipul unui parametru atunci când definiți o funcție. Acest lucru obligă runtime-ul PHP să verifice argumentele transmise unei funcții. Fără specificarea tipului de date, puteți trece orice obiect la orice parametru, ceea ce poate duce la eșecul codului. Având în vedere acest lucru, să examinăm Electronică clasă:

requ_once 'Lights.php'; electronică de clasă funcția turnOn (lumini $ lumini) 

Să implementăm un test:

clasa CarController functie getReadyToGo (motor $ motor, cutie de viteze $ cutie de viteze, electronica electronica $, lumini $ lumini) $ motor-> start (); $ Gearbox-> shift ( 'N'); $ Electronice-> turnOn ($ lumini); return true; 

După cum puteți vedea, getReadyToGo () a folosit funcția $ lumini obiect pentru singurul scop de a le trimite la $ electronica obiecte aprinde() metodă. Este aceasta soluția ideală pentru o astfel de situație? Probabil că nu, dar puteți observa în mod clar modul în care un obiect inactiv, fără nici o legătură cu getReadyToGo () funcția, este transmisă de-a lungul obiectului care are într-adevăr nevoie de el.

Rețineți că toate clasele din CarInterface directorul furnizează obiecte fictive când este inițializat. De asemenea, presupuneți că, pentru acest exercițiu, ne așteptăm ca producătorul să furnizeze clasele reale în viitor. Nu ne putem baza pe lipsa lor actuală de funcționalitate; așa că trebuie să ne asigurăm că testele noastre trec.


Pasul 6: "Stub" statutul și du-te înainte

Un test Stub este un obiect pentru a controla intrarea indirectă a codului testat. Dar ce este inputul indirect? Este o sursă de informații care nu poate fi specificată direct.

Cel mai obișnuit exemplu de test este atunci când un obiect cere alt obiect pentru informații și apoi face ceva cu acele date.

Spionii, prin definiție, sunt mai capabili.

Datele pot fi obținute doar prin solicitarea unui obiect specific și, în multe cazuri, aceste obiecte sunt utilizate pentru un anumit scop în cadrul clasei testate. Nu vrem să "facem noi" (new SomeClass ()) o clasă din interiorul unei alte clase pentru scopuri de testare. Prin urmare, trebuie să injectăm o instanță a unei clase care să acționeze SomeClass fără a injecta un real SomeClass obiect.

Ceea ce vrem este o clasă de stub, care apoi duce la dependență. Injecția de dependență (DI) este o tehnică care injectează un obiect într-un alt obiect, forțându-l să utilizeze obiectul injectat. DI este comună în TDD și este absolut necesară în aproape orice proiect. Acesta oferă o modalitate simplă de a forța un obiect să utilizeze o clasă pregătită pentru testare în locul unei clase reale utilizate în mediul de producție.

Să facem mașina noastră de jucărie să meargă mai departe.

Vrem să implementăm o metodă numită mergi inainte(). Această metodă cere mai întâi a StatusPanel obiect pentru starea combustibilului și a motorului. Dacă autovehiculul este pregătit să meargă, atunci metoda instruiește electronica să accelereze.

Pentru a înțelege mai bine cum funcționează un stub, mai întâi scriu codul pentru verificarea stării și accelerarea:

 funcția goForward (electronică $ electronică) $ statusPanel = new StatusPanel (); dacă ($ statusPanel-> engineIsRunning () && $ statusPanel-> thereIsEnoughFuel ()) $ electronică-> accelera (); 

Acest cod este destul de simplu, dar nu avem un motor sau un combustibil real pentru a ne testa mergi inainte() punerea în aplicare. Codul nostru nu va intra chiar în dacă pentru că nu avem StatusPanel clasă. Dar dacă vom continua cu testul, va începe să apară o soluție logică:

 funcția testItCanAccelerate () $ carController = nou CarController (); $ electronics = electronică nouă (); $ stubStatusPanel = $ acest-> getMock ("StatusPanel"); $ StubStatusPanel-> se asteapta ($ this-> orice ()) -> metoda ( 'thereIsEnoughFuel') -> va ($ this-> returnValue (TRUE)); $ StubStatusPanel-> se asteapta ($ this-> orice ()) -> metoda ( 'engineIsRunning') -> va ($ this-> returnValue (TRUE)); $ carController-> goForward ($ electronică, $ stubStatusPanel); 

Linia de linie explicație:

Îmi place recursivitatea; este întotdeauna mai ușor să testați recursivitatea decât buclele.

  • creaza un nou CarController
  • creați dependența Electronică obiect
  • a crea un mock pentru StatusPanel
  • așteaptă să sune thereIsEnoughFuel () zero sau mai multe ori și să se întoarcă Adevărat
  • așteaptă să sune engineIsRunning () zero sau mai multe ori și să se întoarcă Adevărat
  • apel mergi inainte() cu Electronică și StubbedStatusPanel obiect

Acesta este testul pe care dorim să-l scriem, dar nu va funcționa cu implementarea noastră actuală mergi inainte(). Trebuie să o modificăm:

 funcția goForward (Electronică $ electronică, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : noul StatusPanel (); dacă ($ statusPanel-> engineIsRunning () && $ statusPanel-> thereIsEnoughFuel ()) $ electronică-> accelera (); 

Modificarea noastră folosește dependență prin adăugarea unui al doilea parametru opțional de tip StatusPanel. Determinăm dacă acest parametru are o valoare și creează un nou StatusPanel dacă $ statusPanel este nul. Acest lucru asigură că un nou StatusPanel obiect este creat în producție, permițându-ne totuși să testăm metoda.

Este important să specificați tipul de $ statusPanel parametru. Acest lucru asigură că numai a StatusPanel obiect (sau un obiect al unei clase moștenite) poate fi trecut la metodă. Dar chiar și cu această modificare, testul nostru nu este încă complet.


Pasul 7: Finalizați testul cu un test Real Mock

Trebuie să încercăm o bătaie de cap Electronică obiect pentru a ne asigura metoda de la apelurile de la pasul 6 accelera(). Nu putem folosi adevăratul Electronică din mai multe motive:

  • Nu avem clasa.
  • Nu ne putem verifica comportamentul.
  • Chiar dacă o putem numi, ar trebui să o testăm izolat.

Un martor de test este un obiect care este capabil să controleze atât intrarea și ieșirea indirectă, cât și un mecanism pentru afirmarea automată a așteptărilor și rezultatelor. Această definiție poate părea puțin confuză, dar este foarte simplu de implementat:

 funcția testItCanAccelerate () $ carController = nou CarController (); $ electronics = $ this-> getMock ("Electronică"); $ Electronice-> se așteaptă ($ this-> o dată ()) -> metoda ( 'accelerare'); $ stubStatusPanel = $ acest-> getMock ("StatusPanel"); $ StubStatusPanel-> se asteapta ($ this-> orice ()) -> metoda ( 'thereIsEnoughFuel') -> va ($ this-> returnValue (TRUE)); $ StubStatusPanel-> se asteapta ($ this-> orice ()) -> metoda ( 'engineIsRunning') -> va ($ this-> returnValue (TRUE)); $ carController-> goForward ($ electronică, $ stubStatusPanel); 

Am schimbat pur și simplu $ electronica variabil. În loc să creezi un adevărat Electronică obiect, pur și simplu m-am batjocorit.

Pe următoarea linie, definim o așteptare pe $ electronica obiect. Mai exact, ne așteptăm ca accelera() se numește o singură dată ($ This-> o dată ()). Testul trece acum!

Simțiți-vă liber să jucați cu acest test. Încearcă să schimbi $ This-> o dată () în $ This-> exact (2) și vedeți ce mesaj vă oferă PHPUnit:

1) CarControllerTest :: testItCanAccelerate Așteptările eșuate pentru numele metodei sunt egale cu ; când este invocat (ă) de 2 timp (e). Se aștepta ca metoda să fie numită de 2 ori, numită de fapt de 1 ori.

Pasul 8: Folosiți un test Spy

Un spion de testare este un obiect capabil să capteze ieșirea indirectă și să furnizeze intrări indirecte după cum este necesar.

Producția indirectă este ceva ce nu putem observa în mod direct. De exemplu: când clasa testată calculează o valoare și apoi o folosește ca argument pentru metoda unui alt obiect. Singura modalitate de a observa această ieșire este să cereți obiectului denumit despre variabila utilizată pentru a accesa metoda sa.

Această definiție face ca un spion aproape un mock.

Principala diferență dintre un băiat și un spion este că obiectele machete au aserțiuni și așteptări construite.

În acest caz, cum putem crea un spion de testare folosind PHPUnit getMock ()? Nu putem (nu putem crea un spion pur), dar putem crea bătăi capabile să spioneze alte obiecte.

Să implementăm sistemul de frânare astfel încât să putem opri mașina. Frânarea este foarte simplă; telecomanda va detecta intensitatea frânării de la utilizator și o va trimite controlerului. Telecomanda oferă, de asemenea, o oprire de urgență! buton. Aceasta trebuie să cupleze frânele cu putere maximă.

Valorile de masurare a puterii de franare variaza de la 0 la 100, cu 0 fara nimic si 100 pentru puterea maxima de franare. "Oprirea de urgență!" comanda va fi primită ca apel diferit.

CarController va emite un mesaj către Electronică obiect pentru a activa sistemul de frânare. De asemenea, controlerul auto poate interoga StatusPanel pentru informații privind viteza obținute prin intermediul senzorilor de pe mașină.

Implementarea utilizând un test Spy Pure

Să implementăm mai întâi un obiect spion curat, fără a folosi infrastructura de batjocură a lui PHPUnit. Acest lucru vă va oferi o mai bună înțelegere a conceptului de spion test. Începem prin a verifica Electronică semnătura obiectului.

clasa electronică funcția turnOn (lumini $ lumini)  funcția accelera ()  funcția pushBrakes ($ brakingPower) 

Suntem interesați de pushBrakes () metodă. Nu am spus-o frână() pentru a evita confuzia cu pauză cuvânt cheie în PHP.

Pentru a crea un adevărat spion, ne vom extinde Electronică și suprascrie pushBrakes () metodă. Această metodă depășită nu va împinge frâna; în schimb, va înregistra doar puterea de frânare.

clasa SpyingElectronics extinde Electronics private $ brakingPower; funcția pushBrakes ($ brakingPower) $ this-> brakingPower = $ brakingPower;  funcția getBrakingPower () return $ this-> brakingPower; 

The getBrakingPower () ne dă posibilitatea de a verifica puterea de frânare în testul nostru. Aceasta nu este o metodă pe care o vom folosi în producție.

Acum putem scrie un test capabil să testeze puterea de frânare. Urmând principiile TDD, vom începe cu cel mai simplu test și vom oferi cea mai de bază implementare:

 funcția testItCanStop () $ halfBrakingPower = 50; $ electronicsSpy = noua tehnologie SpyingElectronics (); $ carController = noul CarController (); $ carController-> pushBrake ($ halfBrakingPower, $ electronicsSpy); $ this-> assertEquals ($ jumătateBrakingPower, $ electronicsSpy-> getBrakingPower ()); 

Acest test nu reușește deoarece nu avem încă pushBrakes () metoda pe CarController. Să rectificăm și să scriem una:

 funcția pushBrakes ($ brakingPower, Electronics electronics) $ electronics-> pushBrakes ($ brakingPower); 

Testul trece acum, testând în mod eficient pushBrakes () metodă.

De asemenea, putem spiona apelurile metodice. Testarea StatusPanel clasa este următorul pas logic. Acesta furnizează utilizatorului diferite informații referitoare la telecomanda. Să scriem un test care verifică dacă StatusPanel Obiectul este întrebat despre viteza mașinii. Vom crea un spion pentru asta:

clasa SpyingStatusPanel extinde StatusPanel private $ speedWasRequested = false; funcția getSpeed ​​() $ this-> speedWasRequested = true;  funcția speedWasRequested () return $ this-> speedWasRequested; 

Apoi, modificăm testul pentru a folosi spionul:

 funcția testItCanStop () $ halfBrakingPower = 50; $ electronicsSpy = noua tehnologie SpyingElectronics (); $ statusPanelSpy = nou SpyingStatusPanel (); $ carController = noul CarController (); $ carController-> pushBrake ($ halfBrakingPower, $ electronicsSpy, $ statusPanelSpy); $ this-> assertEquals ($ jumătateBrakingPower, $ electronicsSpy-> getBrakingPower ()); $ This-> assertTrue ($ statusPanelSpy-> speedWasRequested ()); 

Rețineți că nu am scris un test separat.

Recomandarea "o afirmație pe test" este bine de urmat, dar atunci când testul dvs. descrie o acțiune care necesită mai mulți pași sau stări, este acceptabilă utilizarea a mai mult de o afirmație în același test.

Mai mult, acest lucru îți păstrează afirmațiile despre un singur concept într-un singur loc. Acest lucru ajută la eliminarea codului duplicat prin faptul că nu vă obligă să configurați în mod repetat aceleași condiții pentru SUT.

Și acum punerea în aplicare:

 funcția pushBrakes ($ brakingPower, Electronică $ electronică, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : noul StatusPanel (); $ Electronice-> pushBrakes ($ brakingPower); $ StatusPanel-> getSpeed ​​(); 

Există doar un lucru mic și mic care mă deranjează: numele acestui test este testItCanStop (). Asta înseamnă clar că împingem frânele până când mașina se oprește complet. Noi, totuși, am numit metoda pushBrakes (), ceea ce nu este corect. Timp la refactor:

 funcția stop ($ brakingPower, Electronics $ electronics, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : noul StatusPanel (); $ Electronice-> pushBrakes ($ brakingPower); $ StatusPanel-> getSpeed ​​(); 

Nu uitați să schimbați și metoda de apel în test.

$ carController-> oprire ($ halfBrakingPower, $ electronicsSpy, $ statusPanelSpy);

Producția indirectă este ceva ce nu putem observa în mod direct.

În acest moment, trebuie să ne gândim la sistemul nostru de frânare și la modul în care funcționează. Există mai multe posibilități, dar pentru acest exemplu, presupuneți că furnizorul mașinii de jucărie a specificat că frânarea are loc la intervale discrete. Apelarea unui Electronică obiecte pushBreakes () metoda împinge frâna pentru o perioadă discretă de timp și apoi o eliberează. Intervalul de timp este neimportant pentru noi, dar să ne imaginăm că este o fracțiune de secundă. Cu un interval de timp atât de mic, trebuie să trimitem în mod continuu pushBrakes () comenzi până când viteza este zero.

Spionii, prin definiție, sunt stubs mai capabili și pot controla, de asemenea, intrările indirecte dacă este necesar. Să ne facem StatusPanel spion mai capabil și oferă o anumită valoare pentru viteză. Cred că primul apel ar trebui să ofere o viteză pozitivă - să spunem valoarea lui 1. Al doilea apel va oferi viteza de 0.

clasa SpyingStatusPanel extinde StatusPanel private $ speedWasRequested = false; privat $ currentSpeed ​​= 1; funcția getSpeed ​​() if ($ this-> speedWasRequested) $ this-> currentSpeed ​​= 0; $ this-> speedWasRequested = true; returneaza $ this-> currentSpeed;  funcția speedWasRequested () return $ this-> speedWasRequested;  funcția spyOnSpeed ​​() return $ this-> currentSpeed; 

Suprasolicitat getSpeed ​​() metoda returnează valoarea de viteză corespunzătoare prin spyOnSpeed ​​() metodă. Să adăugăm o a treia afirmație la testul nostru:

 funcția testItCanStop () $ halfBrakingPower = 50; $ electronicsSpy = noua tehnologie SpyingElectronics (); $ statusPanelSpy = nou SpyingStatusPanel (); $ carController = noul CarController (); $ carController-> oprire ($ halfBrakingPower, $ electronicsSpy, $ statusPanelSpy); $ this-> assertEquals ($ jumătateBrakingPower, $ electronicsSpy-> getBrakingPower ()); $ This-> assertTrue ($ statusPanelSpy-> speedWasRequested ()); $ this-> assertEquals (0, $ statusPanelSpy-> spyOnSpeed ​​()); 

Conform ultimei afirmații, viteza ar trebui să aibă o valoare a vitezei de 0 după Stop() metoda finalizează execuția. Rularea acestui test împotriva codului nostru de producție duce la un eșec cu un mesaj criptic:

1) CarControllerTest :: testItCanStop Nu a reușit să afirmi că 1 meciuri așteptate 0.

Să adăugăm propriul nostru mesaj de afirmație personalizată:

$ this-> assertEquals (0, $ statusPanelSpy-> spyOnSpeed ​​(), 'Viteza estimată a fi 0 (zero) după oprire, dar de fapt a fost'. $ statusPanelSpy-> spyOnSpeed ​​());

Aceasta produce un mesaj de eșec mult mai lizibil:

1) CarControllerTest :: testItCanStop Viteza estimată a fi 0 (zero) după oprire, dar de fapt a fost 1 A eșuat afirmând că 1 meciuri așteptate 0.

Destule eșecuri! Hai să trecem.

 funcția stop ($ brakingPower, Electronics $ electronics, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : noul StatusPanel (); $ Electronice-> pushBrakes ($ brakingPower); dacă ($ statusPanel-> getSpeed ​​()) $ this-> stop ($ brakingPower, $ electronics, $ statusPanel); 

Îmi place recursivitatea; este întotdeauna mai ușor să testați recursivitatea decât buclele. Testarea mai simplă înseamnă un cod mai simplu, care la rândul său înseamnă un algoritm mai bun. Consultați Premisa Priorității Transformării pentru mai multe informații despre acest subiect.

Revenire la Cadrul Mocking al PHPUnit

Destul cu clasele suplimentare. Să rescriem acest lucru folosind framework-ul de batjocor al lui PHPUnit și să eliminăm acești spioni. De ce?

Deoarece PHPUnit oferă sintaxă de batere mai bună și mai simplă, mai puțin cod și câteva metode predefinite.

Eu, de obicei, creez spioni și pitici numai atunci când le bat joc getMock () ar fi prea complicat. Dacă clasele tale sunt atât de complexe getMock () nu le puteți rezolva, atunci aveți o problemă cu codul dvs. de producție - nu cu testele dvs..

 funcția testItCanStop () $ halfBrakingPower = 50; $ electronicsSpy = $ this-> getMock ("Electronică"); $ ElectronicsSpy-> se asteapta ($ this-> exact (2)) -> Metoda ( 'pushBrakes') -> cu ($ halfBrakingPower); $ statusPanelSpy = $ acest-> getMock ("StatusPanel"); $ StatusPanelSpy-> se asteapta ($ this-> la (0)) -> metoda ( 'getSpeed') -> va ($ this-> returnValue (1)); $ StatusPanelSpy-> se asteapta ($ this-> la (1)) -> metoda ( 'getSpeed') -> va ($ this-> returnValue (0)); $ carController = noul CarController (); $ carController-> oprire ($ halfBrakingPower, $ electronicsSpy, $ statusPanelSpy); 

Totalul tuturor metodelor publice și private, accesibile prin metode publice, reprezintă comportamentul unui obiect.

O explicație line-line a codului de mai sus:

  • seta jumătate de putere de frânare = 50
  • creaza un Electronică a-și bate joc
  • așteaptă metoda pushBrakes () pentru a executa exact de două ori cu puterea de frânare specificată mai sus
  • creeaza o StatusPanel a-și bate joc
  • întoarcere 1 pe prima getSpeed ​​() apel
  • întoarcere 0 pe al doilea getSpeed ​​() execuţie
  • sunați la testat Stop() metoda pe un real CarController obiect

Probabil cel mai interesant lucru din acest cod este $ This-> la ($ someValue) metodă. PHPUnit numără volumul de apeluri la acea machetă. Numărarea se întâmplă la nivel de machet; astfel, apelând la mai multe metode $ statusPanelSpy ar creste contorul. Acest lucru poate părea un început contra-intuitiv la început; așa că haideți să vedem un exemplu.

Presupunem că vrem să verificăm nivelul combustibilului la fiecare apel la Stop(). Codul ar arăta astfel:

 funcția stop ($ brakingPower, Electronics $ electronics, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : noul StatusPanel (); $ Electronice-> pushBrakes ($ brakingPower); $ StatusPanel-> thereIsEnoughFuel (); dacă ($ statusPanel-> getSpeed ​​()) $ this-> stop ($ brakingPower, $ electronics, $ statusPanel); 

Acest lucru ne va rupe testul. Ați putea fi confuz de ce, dar veți primi următorul mesaj:

1) CarControllerTest :: testItCanStop Așteptările eșuate pentru numele metodei sunt egale cu  când este invocat (ă) de 2 timp (e). Se aștepta ca metoda să fie numită de 2 ori, numită de fapt de 1 ori.

Este destul de evident că pushBrakes () ar trebui să fie numit de două ori. De ce primim acest mesaj? Din cauza $ This-> la ($ someValue) așteptare. Counter-ul crește după cum urmează:

  • primul apel la Stop() -> primul apel către thereIsEnougFuel () => contorul intern la 0
  • primul apel la Stop() -> primul apel către getSpeed ​​() => contorul intern la 1 și întoarcere 0
  • al doilea apel la Stop() nu se întâmplă niciodată => al doilea apel la getSpeed ​​() nu se întâmplă niciodată

Fiecare apel la orice mocked metoda pe $ statusPanelSpy incrementează contorul intern al PHPUnit.


Pasul 9: Un test fals

Dacă metodele publice sunt similare cu mesajele, atunci metodele private sunt similare cu cele ale gândurilor.

Un fals test este o implementare mai simplă a unui obiect de cod de producție. Aceasta este o definiție foarte asemănătoare pentru a testa stubs. În realitate, Fake și Stubs sunt foarte asemănătoare ca pe un comportament extern. Ambele obiecte imită comportamentul altor obiecte reale și ambele implementează o metodă de control al intrării indirecte. Diferența este că Fake-urile sunt mult mai aproape de un obiect real decât de un obiect inactiv.

Un Stub este în esență un obiect dummy a cărui metodă returnează valori predefinite. Un Fake, cu toate acestea, face o implementare completă a unui obiect real într-un mod mult mai simplu. Probabil cel mai frecvent exemplu este u

Cod