Cadrele oferă un instrument pentru dezvoltarea rapidă a aplicațiilor, dar adesea acumulează datorii tehnice cât de repede vă permit să creați funcționalități. Datoria tehnică este creată atunci când mentenabilitatea nu este o atenție specială a dezvoltatorului. Schimbările viitoare și depanarea devin costisitoare, din cauza lipsei de testare și structură unitară.
Iată cum puteți începe structurarea codului pentru a obține testabilitate și mentenabilitate - și pentru a vă economisi timp.
Să începem cu niște coduri contrived, dar tipice. Aceasta ar putea fi o clasă de model în orice cadru dat.
user class funcția publică getCurrentUser () $ user_id = $ _SESSION ['user_id']; $ user = App :: db-> select ('id, username') -> unde ('id', $ user_id) -> limită (1) -> get (); dacă ($ user-> num_results ()> 0) return $ user-> row (); return false;
Acest cod va funcționa, dar are nevoie de îmbunătățire:
$ _SESSION
variabilă globală. Unitățile de testare a cadrelor, cum ar fi PHPUnit, se bazează pe linia de comandă, unde $ _SESSION
și multe alte variabile globale nu sunt disponibile.App :: db
utilizate în aplicația noastră. De asemenea, cum rămâne cu situațiile în care nu vrem doar informația curentă a utilizatorului?Iată o încercare de a crea un test de unitate pentru funcționalitatea de mai sus.
class UserModelTest extinde PHPUnit_Framework_TestCase funcția publică testGetUser () $ user = new User (); $ curentUser = $ user-> getCurrentUser (); $ this-> assertEquals (1, $ curentUser-> id);
Să examinăm asta. În primul rând, testul va eșua. $ _SESSION
variabilă folosită în Utilizator
obiect nu există într-un test de unitate, deoarece rulează PHP în linia de comandă.
În al doilea rând, nu există setări de conectare la baza de date. Aceasta înseamnă că, pentru a face acest lucru, va trebui să ne punem în aplicare cererea pentru a obține App
obiect și a lui db
obiect. Vom avea nevoie, de asemenea, de o conexiune la baza de date de lucru pentru a testa împotriva.
Pentru ca această unitate să funcționeze la test, ar trebui să:
Deci, haideți să ajungem la modul în care putem îmbunătăți acest lucru.
Funcția de preluare a utilizatorului curent nu este necesară în acest context simplu. Acesta este un exemplu controversat, dar în spiritul principiilor DRY, prima optimizare pe care o aleg să o fac este generalizarea acestei metode.
($ user_id) $ user = App :: db-> select ('user') -> unde ('id', $ user_id) -> limită (1) -> get (); dacă ($ user-> num_results ()> 0) return $ user-> row (); return false;
Aceasta oferă o metodă pe care o putem folosi în întreaga noastră aplicație. Putem trece la utilizatorul curent în momentul apelului, mai degrabă decât trecerea acestei funcționalități la model. Codul este mai modular și menținut atunci când nu se bazează pe alte funcționalități (cum ar fi variabila globală a sesiunii).
Cu toate acestea, acest lucru nu este încă testabil și menținut așa cum ar putea fi. Încă ne bazăm pe conexiunea bazei de date.
Să ajutăm la îmbunătățirea situației prin adăugarea unei injecții de dependență. Iată cum ar putea arăta modelul nostru, atunci când trecem conectivitatea bazei de date în clasă.
user class protejat $ _db; funcția publică __construct ($ db_connection) $ this -> _ db = $ db_connection; funcția publică getUser ($ user_id) $ user = $ this -> _ db-> select ('user') -> unde ('id', $ user_id) -> limit (1) -> get; dacă ($ user-> num_results ()> 0) return $ user-> row (); return false;
Acum, dependențele noastre Utilizator
model sunt prevăzute. Clasa noastră nu mai presupune o anumită conexiune la baza de date, nici nu se bazează pe obiecte globale.
În acest moment, clasa noastră este practic testabilă. Putem trece într-o sursă de date la alegerea noastră (cea mai mare parte) și un id de utilizator și să testați rezultatele acelui apel. De asemenea, putem schimba conexiuni separate de baze de date (presupunând că ambele implementează aceleași metode pentru recuperarea datelor). Rece.
Să ne uităm la ce ar putea arăta un test de unitate pentru asta.
_mockDb (); $ user = utilizator nou ($ db_connection); $ rezultat = $ user-> getUser (1); $ așteptat = nou StdClass (); $ așteptată-> id = 1; $ expected-> username = 'fideloper'; $ this-> assertEquals ($ rezultat-> id, $ așteptat-> id, "Setarea corectă a codului de utilizator"); $ this-> assertEquals ($ rezultat-> numele de utilizator, $ expected-> username, 'Setul corect de utilizator'); funcția protejată _mockDb () // "Mock" (stub) rezultatul obiectului $ row returnResult = new StdClass (); $ returnResult-> id = 1; $ returnResult-> nume utilizator = 'fideloper'; // Obiectul rezultat al bazei de date mock $ result = m :: mock ('DbResult'); $ result-> shouldReceive ('num_results') -> once () -> șiReturn (1); $ result-> shouldReceive ('row') -> o dată () -> șiReturn ($ returnResult); // Obiect conexiune bază de date bază de date $ db = m :: mock ('DbConnection'); $ db-> shouldReceive ('selectează') -> o dată () -> șiReturn ($ db); $ db-> shouldReceive ('unde') -> o dată () -> șiReturn ($ db); $ db-> shouldReceive ('limit') -> o dată () -> șiReturn ($ db); $ db-> shouldReceive ('get') -> o dată () -> șiReturn ($ rezultat); returnați $ db;
Am adăugat ceva nou în acest test de unitate: Bătrâni. Dăruirea vă permite să "falsificați" (false) obiectele PHP. În acest caz, batem conexiunea bazei de date. Cu martorul nostru, putem trece peste testarea unei conexiuni de baze de date și pur și simplu testați modelul nostru.
Doriți să aflați mai multe despre Mockery?
În acest caz, ne batem o conexiune SQL. Spunem obiectului fals să se aștepte să aibă Selectați
, Unde
, limită
și obține
metodele pe care le-a numit. Mă întorc pe Mock, în sine, pentru a reflecta modul în care obiectul SQL se întoarce ($ this
), făcând astfel metoda sa "în lanț". Rețineți că, pentru obține
, returnez rezultatul apelului bazei de date - a stdClass
obiect cu datele de utilizator populate.
Acest lucru rezolvă câteva probleme:
Încă mai putem face mult mai bine. Iată unde devine interesant.
Pentru a îmbunătăți acest lucru, am putea defini și implementa o interfață. Luați în considerare următorul cod.
interfață UserRepositoryInterface funcția publică getUser ($ user_id); class MysqlUserRepository implementează UserRepositoryInterface protejat $ _db; funcția publică __construct ($ db_conn) $ this -> _ db = $ db_conn; funcția publică getUser ($ user_id) $ user = $ this -> _ db-> select ('user') -> unde ('id', $ user_id) -> limit (1) -> get; dacă ($ user-> num_results ()> 0) return $ user-> row (); return false; Utilizator de clasă protected $ userStore; funcția publică __construct (UserRepositoryInterface $ user) $ this-> userStore = $ user; funcția publică getUser ($ user_id) retur $ this-> userStore-> getUser ($ user_id);
Sunt câteva lucruri care se întâmplă aici.
Adăugați utilizator()
metodă.Interfața cu utilizatorul
în a noastră Utilizator
model. Acest lucru garantează că sursa de date va avea întotdeauna a getUser ()
Metoda disponibilă, indiferent de sursa de date utilizată pentru implementare Interfața cu utilizatorul
.Rețineți că noi
Utilizator
tipul de sfaturi de tipInterfața cu utilizatorul
în constructorul său. Aceasta înseamnă că o clasă de implementareInterfața cu utilizatorul
TREBUIE să fie transmise înUtilizator
obiect. Aceasta este o garanție pe care ne bazăm - avem nevoie de eagetUser
pentru a fi întotdeauna disponibilă.
Care este rezultatul acestui lucru?
Utilizator
clasa, putem mica cu ușurință sursa de date. (Testarea implementărilor sursei de date ar fi treaba unui test separat al unității).Utilizator
obiect dacă trebuie. Dacă decideți să ștergeți SQL, puteți crea o implementare diferită (de exemplu, MongoDbUser
) și treci asta în tine Utilizator
model.Am simplificat și testul de unitate!
_mockUserRepo (); $ user = utilizator nou ($ userRepo); $ rezultat = $ user-> getUser (1); $ așteptat = nou StdClass (); $ așteptată-> id = 1; $ expected-> username = 'fideloper'; $ this-> assertEquals ($ rezultat-> id, $ așteptat-> id, "Setarea corectă a codului de utilizator"); $ this-> assertEquals ($ rezultat-> numele de utilizator, $ expected-> username, 'Setul corect de utilizator'); funcția protejată _mockUserRepo () // Mock rezultatul așteptat $ result = new StdClass (); $ rezultat-> id = 1; $ result-> username = 'fideloper'; // Mock orice depozit de utilizatori $ userRepo = m :: mock ('Fideloper \ Third \ Repository \ UserRepositoryInterface'); $ userRepo-> shouldReceive ('getUser') -> o dată () -> șiReturn ($ rezultat); returnează $ userRepo;
Am luat munca de a batjocori complet o conexiune de baze de date. În schimb, pur și simplu batem sursa de date și spuneți-i ce să facă când getUser
se numește.
Dar, putem face tot mai bine!
Luați în considerare utilizarea codului nostru actual:
// În unele controler $ user = new User (nou MysqlUser (App: db-> getConnection ("mysql"))); $ user-> id = App :: sesiune ("user-> id"); $ curentUser = $ user-> getUser ($ user_id);
Ultimul nostru pas va fi să introducem containere. În codul de mai sus, trebuie să creați și să utilizați o grămadă de obiecte doar pentru a obține utilizatorul nostru curent. Acest cod poate fi îngrădit în aplicația dvs. Dacă trebuie să treceți de la MySQL la MongoDB, veți face asta încă trebuie să editați fiecare loc unde apare codul de mai sus. Nu e prea uscat. Containerele pot repara acest lucru.
Un container pur și simplu "conține" un obiect sau o funcționalitate. Este similar cu un registru din aplicația dvs. Putem folosi un container pentru a instanția automat un nou Utilizator
obiect cu toate dependențele necesare. Mai jos, folosesc Pimple, o clasă populară de containere.
// undeva într-un fișier de configurare $ container = nou Pimple (); $ container ["user"] = functie () retur nou utilizator (nou MysqlUser (app: db-> getConnection ('mysql')); Acum, la toți controlorii noștri, putem scrie: $ currentUser = $ container ['user'] -> getUser (App :: session ('user_id'));
Am mutat crearea Utilizator
model într-o singură locație în configurația aplicației. Ca rezultat:
Utilizator
obiect și depozitul de date de alegere este definit într-o locație din aplicația noastră.Utilizator
model de la utilizarea MySQL la orice altă sursă de date în UNU Locație. Acest lucru este mult mai sustenabil.În cursul acestui tutorial am realizat următoarele:
Sunt sigur că ați observat că am adăugat mult mai mult cod în numele mentenabilității și testabilității. Se poate face un argument puternic împotriva acestei implementări: intensificăm complexitatea. Într-adevăr, aceasta necesită o cunoaștere mai aprofundată a codului, atât pentru autorul principal, cât și pentru colaboratorii unui proiect.
Cu toate acestea, costul de explicație și de înțelegere este cu mult depășit de cotele de suplimentare în ansamblu scădea în datoria tehnică.
Puteți include Bătaie de joc și PHPUnit în aplicația dvs. cu ușurință folosind Composer. Adăugați-le în secțiunea "necesită-dev" din secțiunea dvs. composer.json
fişier:
"require-dev": "batjocură / batjocură": "0.8. *", "phpunit / phpunit": "3.7.
Apoi, puteți instala dependențele bazate pe Composer cu cerințele "dev":
$ php compune.phar install --dev
Aflați mai multe despre Mockery, Composer și PHPUnit aici pe Nettuts+.
Pentru PHP, vă recomandăm să folosiți Laravel 4, deoarece utilizează în mod excepțional containerele și alte concepte scrise aici.
Vă mulțumim pentru lectură!