În acest tutorial, voi prezenta un exemplar end-to-end al unei aplicații simple - făcută strict cu TDD în PHP. Te voi îndruma în fiecare pas, unul câte unul, în timp ce explică deciziile pe care le-am luat pentru a-mi îndeplini sarcina. Exemplul respectă regulile TDD: teste de scriere, cod de scriere, refactor.
TDD este o tehnică "test-first" pentru dezvoltarea și proiectarea de software. Este aproape întotdeauna folosit în echipe agile, fiind unul dintre instrumentele principale ale dezvoltării software-ului agil. TDD a fost pentru prima dată definită și introdusă în comunitatea profesională de Kent Beck în 2002. De atunci, ea a devenit o tehnică acceptată și recomandată în programarea zilnică.
TDD are trei reguli principale:
PHPUnit este instrumentul care permite programatorilor PHP să efectueze testarea unităților și să practice practici de testare. Acesta este un cadru complet de testare a unității, cu suport de batjocură. Chiar dacă există câteva opțiuni alternative, PHPUnit este soluția cea mai utilizată și cea mai completă pentru PHP astăzi.
Pentru a instala PHPUnit, puteți urma fie împreună cu tutorialul anterior în sesiunea noastră "TDD în PHP", fie puteți utiliza PEAR, după cum se explică în documentația oficială:
rădăcină
sau utilizare sudo
pere de upgrade PEAR
perete config-set auto_discover 1
pere a instala pear.phpunit.de/PHPUnit
Mai multe informații și instrucțiuni pentru instalarea modulelor suplimentare PHPUnit pot fi găsite în documentația oficială.
Unele distribuții Linux oferă PHPUnit ca pachet precomprimat, deși întotdeauna recomand o instalare prin intermediul PEAR, deoarece asigură instalarea și utilizarea celei mai recente și actualizate versiuni.
Dacă sunteți un fan al NetBeans, îl puteți configura pentru a funcționa cu PHPUnit urmând acești pași:
Dacă nu utilizați un IDE cu suport pentru testarea unităților, puteți rula întotdeauna testul direct din consola:
cd / my / aplicații / test / folder phpunit
Echipa noastră este însărcinată cu implementarea unei funcții "wrap wrap".
Să presupunem că facem parte dintr-o mare corporație, care are o aplicație sofisticată de dezvoltare și întreținere. Echipa noastră este însărcinată cu implementarea unei funcții "wrap wrap". Clienții noștri nu doresc să vadă barele de derulare orizontale și este obligat să se conformeze.
În acest caz, trebuie să creați o clasă care să fie capabilă să formateze un bit arbitrar de text furnizat ca intrare. Rezultatul ar trebui să fie un cuvânt înfășurat la un număr specificat de caractere. Regulile de ambalare a cuvintelor ar trebui să urmeze comportamentul altor aplicații de zi cu zi, cum ar fi editorii de text, zonele de text ale paginilor web etc. Clientul nostru nu înțelege toate regulile de ambalare a cuvintelor, dar ei știu că o doresc și ei o cunosc ar trebui să funcționeze în același mod pe care l-au experimentat în alte aplicații.
TDD vă ajută să realizați un design mai bun, dar nu elimină necesitatea de a crea un design și de a gândi în față.
Unul dintre lucrurile pe care mulți programatori uită, după ce au început TDD, este să gândească și să planifice în prealabil. TDD vă ajută să realizați un design mai bun de cele mai multe ori, cu mai puțin cod și funcționalitate verificată, dar nu elimină necesitatea designului și a gândirii umane.
De fiecare dată când trebuie să rezolvați o problemă, ar trebui să vă alocați timp pentru a vă gândi la asta, pentru a vă imagina un mic design - nimic fantezist - dar suficient pentru a vă începe. Această parte a locului de muncă vă ajută să vă imaginați și să ghiciți scenarii posibile pentru logica aplicației.
Să ne gândim la regulile de bază pentru o caracteristică de împachetare a cuvintelor. Presupun că ni se va da un text înfășurat. Vom cunoaște numărul de caractere pe linie și vom dori să fie înfășurat. Deci, primul lucru care îmi vine în minte este că, dacă textul are mai multe caractere decât numărul dintr-o singură linie, ar trebui să adăugăm o nouă linie în loc de ultimul caracter spațial care este încă pe linie.
Bine, asta ar rezuma comportamentul sistemului, dar este mult prea complicat pentru orice test. De exemplu, atunci când un singur cuvânt este mai lung decât numărul de caractere permise pe o linie? Hmmm ... asta arata ca un caz de margine; nu putem înlocui un spațiu cu o linie nouă, deoarece nu avem spații pe acea linie. Ar trebui să împovărăm cuvântul, împărțind-o efectiv în două.
Aceste idei ar trebui să fie suficient de clare pentru a putea începe programarea. Avem nevoie de un proiect și de o clasă. Să spunem asta ambalaj
.
Să ne creăm proiectul. Ar trebui să existe un director principal pentru clasele sursă și a teste /
dosar, firește, pentru teste.
Primul fișier pe care îl vom crea este un test în cadrul teste
pliant. Toate încercările viitoare vor fi conținute în acest dosar, așa că nu voi mai specifica acest lucru în mod explicit în acest tutorial. Denumiți clasa de testare ceva descriptiv, dar simplu. WrapperTest
va face pentru moment; primul nostru test arată astfel:
requ_once dirname (__ FILE__). '/ ... / Wrapper.php'; clasa WrapperTest extinde PHPUnit_Framework_TestCase function testCanCreateAWrapper () $ wrapper = new Wrapper ();
Tine minte! Nu avem voie să scriem niciun cod de producție înainte de o încercare nereușită - nici măcar o declarație de clasă! De aceea am scris primul test simplu de mai sus, numit canCreateAWrapper
. Unii consideră că acest pas este inutil, dar consider că este o ocazie frumoasă să ne gândim la clasa pe care o vom crea. Avem nevoie de o clasă? Ce ar trebui să numim asta? Ar trebui să fie statică?
Când executați testul de mai sus, veți primi un mesaj de eroare fatală, după cum urmează:
PHP Eroare fatală: require_once (): Necesită deschidere '/ path / to / WordWrapPHP / Teste / ... /Wrapper.php' (include_path = '.: / Usr / share / php5: / usr / share / php' cale / spre / WordWrapPHP / Teste / WrapperTest.php pe linia 3
Hopa! Ar trebui să facem ceva în legătură cu asta. Creați un spațiu gol ambalaj
clasă în dosarul principal al proiectului.
clasă Wrapper
Asta e. Dacă executați din nou testul, acesta trece. Felicitări pentru primul test!
Deci, proiectul nostru a fost creat și rulat; acum trebuie să ne gândim la primul nostru real Test.
Care ar fi cel mai simplu ... cel mai prost ... testul cel mai de bază care ar face ca actualul nostru cod de producție să nu reușească? Ei bine, primul lucru care vine în minte este "Dați-i un cuvânt suficient de scurt și așteptați ca rezultatul să rămână neschimbat."Suna posibil, să scriem testul.
requ_once dirname (__ FILE__). '/ ... / Wrapper.php'; Clasa WrapperTest extinde PHPUnit_Framework_TestCase function testDoesNotWrapAShorterThanMaxCharsWord () $ wrapper = nou Wrapper (); assertEquals ('cuvânt', $ wrapper-> wrap ('cuvânt', 5));
Asta pare destul de complicat. Ce înseamnă "MaxChars" în numele funcției? Ce face 5
în împacheta
metoda se referă la?
Cred că ceva nu este chiar aici. Nu există o încercare mai simplă pe care să o putem executa? Da, cu siguranță există! Dacă am înfășura ... nimic - un șir gol? Asta suna bine. Ștergeți testul complicat de mai sus și, în schimb, adăugați noul nostru, mai simplu, prezentat mai jos:
requ_once dirname (__ FILE__). '/ ... / Wrapper.php'; clasa WrapperTest extinde PHPUnit_Framework_TestCase function testItShouldWrapAnEmptyString () $ wrapper = new Wrapper (); $ this-> assertEquals (", $ wrapper-> wrap ("));
Acest lucru este mult mai bun. Numele testului este ușor de înțeles, nu avem șiruri magice sau numere și, mai presus de toate, nu funcționează corect!
Eroare fatală: Apel la metoda nedefinită Wrapper :: wrap () în ...
După cum puteți observa, am șters primul nostru test. Este inutil să verificăm în mod explicit dacă un obiect poate fi inițializat, când și alte teste au nevoie de el. Asta este normal. Cu timpul, veți găsi că ștergerea testelor este un lucru obișnuit. Testele, în special testele unitare, trebuie să se desfășoare rapid - foarte repede ... și frecvent - foarte frecvent. Având în vedere acest lucru, eliminarea redundanței în teste este importantă. Imaginați-vă că executați mii de teste de fiecare dată când salvați proiectul. Nu ar trebui să dureze mai mult de două minute, maximum, pentru ca ei să ruleze. Deci, nu vă îngroziți să ștergeți un test, dacă este necesar.
Revenind la codul nostru de producție, să facem acel test:
clasa Wrapper wrap wrap (text $) return;
Mai sus, am adăugat absolut nici un cod mai mult decât este necesar pentru a face testul.
Acum, pentru următorul test de eșec:
funcția testItDoesNotWrapAShortEnoughWord () $ wrapper = nou Wrapper (); $ this-> assertEquals ('cuvânt', $ wrapper-> wrap ('cuvânt', 5));
Mesaj de eroare:
Nu a reușit să afirme că meciurile nula așteptate "cuvânt".
Și codul care o face să treacă:
funcția wrap (text $) return $ text;
Wow! A fost ușor, nu-i așa??
În timp ce suntem în verde, observați că codul nostru de testare poate începe să putrezească. Trebuie să refacem câteva lucruri. Amintiți-vă: întotdeauna refactor atunci când testele dvs. trece; aceasta este singura modalitate prin care puteți fi siguri că ați refactat corect.
Mai întâi, să eliminăm duplicarea inițializării obiectului de împachetare. Putem face acest lucru doar o singură dată în înființat()
și utilizați-l pentru ambele teste.
clasa WrapperTest extinde PHPUnit_Framework_TestCase private $ wrapper; funcția setUp () $ this-> wrapper = nou Wrapper (); funcția testItShouldWrapAnEmptyString () $ this-> assertEquals (", $ this-> wrapper-> wrap (")); funcția testItDoesNotWrapAShortEnoughWord () $ this-> assertEquals ('cuvânt', $ this-> wrapper-> wrap ('cuvânt', 5));
înființat
metoda se va executa înainte de fiecare nouă încercare.
Apoi, există alți biți ambigui în al doilea test. Ce este "cuvânt"? Ce este "5"? Să clarificăm, astfel încât următorul programator care citește aceste teste nu trebuie să ghicească.
Nu uitați că testele dvs. sunt, de asemenea, cea mai actualizată documentație pentru codul dvs..Un alt programator ar trebui să poată citi testele la fel de ușor cum ar citi documentația.
funcția testItDoesNotWrapAShortEnoughWord () $ textToBeParsed = 'cuvânt'; $ maxLineLength = 5; $ this-> assertEquals ($ textToBeParsed, $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Acum citiți din nou această afirmație. Nu citește mai bine? Bineînțeles. Nu vă fie teamă de numele variabilelor lungi pentru testele dvs.; auto-finalizarea este prietenul tău! Este mai bine să fii cât mai descriptiv posibil.
Acum, pentru următorul test de eșec:
funcția testItWrapsAWordLongerThanLineLength () $ textToBeParsed = 'alongword'; $ maxLineLength = 5; $ this-> assertEquals ("de-a lungul \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Și codul care o face să treacă:
($ text, $ lineLength) if (strlen ($ text)> $ lineLength) returnează substr ($ text, 0, $ lineLength). "\ n". substr ($ text, $ lineLength); returnați $ text;
Acesta este codul evident pentru a ne face ultimul teste de încercare. Dar fii atent - este și codul care face primul nostru test nu trece!
Avem două opțiuni pentru a rezolva această problemă:
Dacă alegeți prima opțiune, făcând parametrul opțional, aceasta ar prezenta o mică problemă cu codul curent. Un parametru opțional este, de asemenea, inițializat cu o valoare implicită. Ce ar putea fi o astfel de valoare? Zero s-ar putea suna logic, dar ar presupune scrierea unui cod doar pentru a trata acel caz special. Setarea unui număr foarte mare, astfel încât primul dacă declarația nu ar avea drept rezultat poate fi o altă soluție. Dar, care este numărul ăsta? Este 10? Este 10000? Este 10000000? Nu putem spune cu adevărat.
Având în vedere toate acestea, voi modifica primul test:
funcția testItShouldWrapAnEmptyString () $ this-> assertEquals (", $ this-> wrapper-> wrap (", 0));
Din nou, toate verde. Acum putem trece la următorul test. Să ne asigurăm că, dacă avem un cuvânt foarte lung, se va împacheta pe mai multe rânduri.
funcția testItWrapsAWordSeveralTimesIfItsTooLong () $ textToBeParsed = 'averyverylongword'; $ maxLineLength = 5; $ this-> assertEquals ("avery \ nveryl \ nongwo \ nrd", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Acest lucru nu reușește în mod evident, deoarece codul nostru de producție actuală se împachetează doar o singură dată.
Nu s-a afirmat că două șiruri sunt egale. --- Așteptat +++ Actual @@ @@ 'avery -veryl -ongwo -rd' + verylongword '
Poți mirosi in timp ce
bucla vine? Ei bine, gândiți-vă din nou. Este a in timp ce
buclă cel mai simplu cod care ar face trecerea testului?
Potrivit "priorităților de transformare" (de Robert C. Martin), nu este așa. Recurgerea este mereu mai simplă decât o buclă și este mult mai probabilă.
($ text, $ lineLength) if (strlen ($ text)> $ lineLength) returnează substr ($ text, 0, $ lineLength). "\ n". $ this-> wrap (substr (text $, $ lineLength), $ lineLength); returnați $ text;
Poți chiar să observi schimbarea? Era simplu. Tot ce am făcut a fost, în loc să concatenăm cu restul șirului, să concatenăm cu valoarea returnată de a ne chema cu restul șirului. Perfect!
Următorul test simplu? Ce zici de două cuvinte se pot înfășura, când există un spațiu la capătul liniei.
function testItWrapsTwoWordsWhenSpaceAtTheEndOfLine () $ textToBeParsed = 'cuvânt cuvânt'; $ maxLineLength = 5; $ this-> assertEquals ("cuvânt \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Asta se potrivește frumos. Cu toate acestea, soluția ar putea deveni puțin mai dificilă de data aceasta.
La început, puteți să vă referiți la a str_replace ()
pentru a scăpa de spațiu și pentru a introduce o nouă linie. Nu-i; că drumul duce la un sfârșit mort.
A doua opțiune cea mai evidentă ar fi una dacă
afirmație. Ceva de genul:
($ text, 0, strpos ($ text, ")). În cazul în care se utilizează textul, textul va fi redactat. "\ n". $ this-> wrap (substr ($ text, strpos ($ text, ") + 1), $ lineLength); \ n ". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength); retur $ text;
Cu toate acestea, aceasta intră într-o buclă fără sfârșit, ceea ce va determina erorile testelor.
Eroare eroare fatală: dimensiunea permisă a memoriei de 134217728 octeți epuizată
De data aceasta, trebuie să ne gândim! Problema este că primul nostru test are un text cu o lungime de zero. De asemenea, strpos ()
returnează fals când nu poate găsi șirul. Comparând falsul cu zero ... este? Este Adevărat
. Acest lucru este rău pentru noi, deoarece bucla va deveni infinită. Soluția? Să schimbăm prima condiție. În loc să căutați un spațiu și să îi comparați poziția cu lungimea liniei, să încercăm să luăm în mod direct caracterul în poziția indicată de lungimea liniei. Vom face asta substr ()
doar un singur caracter, începând de la locul exact din text.
($ text, 0, strpos ($ text, ")) returnează substr ($ text, 0, strpos ($ text,")). "\ n". $ this-> wrap (substr ($ text, strpos ($ text, ") + 1), $ lineLength); \ n ". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength); retur $ text;
Dar, dacă spațiul nu este corect la sfârșitul liniei?
funcția testItWrapsTwoWordsWhenLineEndIsAfterFirstWord () $ textToBeParsed = 'cuvânt cuvânt'; $ maxLineLength = 7; $ this-> assertEquals ("cuvânt \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Hmm ... trebuie să ne revizuim din nou condițiile. Mă gândesc că, la urma urmei, vom avea nevoie de căutarea poziției caracterului spațial.
($ text, $ lineLength) if (strlen ($ text)> $ lineLength) if (strpos (substr ($ text, 0, $ lineLength) , strpos ($ text, ")). "\ n". $ this-> wrap (substr ($ text, strpos ($ text, ") + 1), $ lineLength) return substr ($ text, 0, $ lineLength). ($ text, $ lineLength), $ lineLength); returnați $ text;
Wow! Asta chiar funcționează. Am mutat prima condiție în cea de-a doua, pentru a evita buclă fără sfârșit și am adăugat căutarea spațiului. Totuși, pare destul de urât. Condiții nesolicitate? Yuck. E timpul pentru unele refactorizări.
funcția wrap ($ text, $ lineLength) if (strlen ($ text) <= $lineLength) return $text; if (strpos(substr($text, 0, $lineLength),") != 0) return substr ($text, 0, strpos($text,")) . "\n" . $this->(substr ($ text, strpos ($ text, ") + 1), $ lineLength), return substr ($ text, 0, $ lineLength) $ lineLength), $ lineLength);
E mai bine.
Nu se poate întâmpla nimic rău ca rezultat al unui test.
Următorul test simplu ar fi să aveți trei cuvinte înfășurate pe trei rânduri. Dar testul trece. Ar trebui să scrieți un test când știți că va trece? De cele mai multe ori, nu. Dar, dacă aveți îndoieli sau vă puteți imagina modificări evidente ale codului care ar face ca noul test să nu reușească și ceilalți să treacă, atunci scrieți-l! Nu se poate întâmpla nimic rău ca rezultat al unui test. De asemenea, considerați că testele dvs. sunt documentația dvs. Dacă testul dvs. reprezintă o parte esențială a logicii dvs., atunci scrieți-o!
În plus, faptul că testele la care am venit au trecut, este un indiciu că ne apropiem de o soluție. Evident, atunci când aveți un algoritm de lucru, orice test pe care îl scriem va trece.
Acum - trei cuvinte pe două linii, cu linia care se termină în ultimul cuvânt; acum, că nu reușește.
function testItWraps3WordsOn2Lines () $ textToBeParsed = 'cuvânt cuvânt cuvânt'; $ maxLineLength = 12; $ this-> assertEquals ("cuvânt cuvânt \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Aproape am așteptat ca acesta să lucreze. Când investigăm eroarea, obținem:
Nu s-a afirmat că două șiruri sunt egale. --- Expected +++ Actual @@ @@ - cuvânt cuvânt-cuvânt "+" cuvânt + cuvânt cuvânt "
Da. Ar trebui să înfășurăm într-o linie linia dreaptă.
funcția wrap ($ text, $ lineLength) if (strlen ($ text) <= $lineLength) return $text; if (strpos(substr($text, 0, $lineLength),") != 0) return substr ($text, 0, strrpos($text,")) . "\n" . $this->(substr ($ text, strrpos ($ text, ") + 1), $ lineLength); return substr ($ text, 0, $ lineLength) $ lineLength), $ lineLength);
Pur și simplu înlocuiți strpos ()
cu strrpos ()
în al doilea dacă
afirmație.
Lucrurile devin mai complicate. Este destul de greu să găsești un test de eșec ... sau orice test, de altfel, care nu a fost încă scris.
Acesta este un indiciu că suntem aproape de o soluție finală. Dar, hei, m-am gândit la un test care va eșua!
funcția testItWraps2WordsOn3Lines () $ textToBeParsed = 'cuvânt cuvânt'; $ maxLineLength = 3; $ this-> assertEquals ("wor \ nd \ nwor \ nd", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Dar m-am înșelat. Ea trece. - Am terminat? Aștepta! Ce zici de asta?
function testItWraps2WordsAtBoundry () $ textToBeParsed = 'cuvânt cuvânt'; $ maxLineLength = 4; $ this-> assertEquals ("cuvânt \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Eșuează! Excelent. Când linia are aceeași lungime ca și cuvântul, vrem ca a doua linie să nu înceapă cu un spațiu.
Nu s-a afirmat că două șiruri sunt egale. --- Așteptat +++ Actual @@ @@ 'cuvânt-cuvânt' + wor + d '
Există mai multe soluții. Am putea introduce un altul dacă
declarație pentru a verifica spațiul de pornire. Acest lucru ar fi în concordanță cu restul condițiilor pe care le-am creat. Dar, nu există o soluție mai simplă? Dacă doar noi tunde()
textul?
($ text, $ lineLength) $ text = trim ($ text); dacă (strlen (text $) <= $lineLength) return $text; if (strpos(substr($text, 0, $lineLength),") != 0) return substr ($text, 0, strrpos($text,")) . "\n" . $this->(substr ($ text, strrpos ($ text, ") + 1), $ lineLength); return substr ($ text, 0, $ lineLength) $ lineLength), $ lineLength);
Vom merge acolo.
În acest moment, nu pot inventa nici un test de eșec pentru a scrie. Trebuie să fim! Am folosit TDD pentru a construi un algoritm simplu, dar util, de șase linii.
Câteva cuvinte despre oprirea și "se face". Dacă folosiți TDD, vă obligați să vă gândiți la tot felul de situații. Apoi scrieți teste pentru acele situații și, în acest proces, începeți să înțelegeți problema mult mai bine. De obicei, acest proces are ca rezultat o cunoaștere intimă a algoritmului. Dacă nu vă puteți gândi la alte teste care nu reușesc să scrieți, înseamnă că algoritmul dvs. este perfect? Nu este necesar, cu excepția cazului în care există un set predefinit de reguli. TDD nu garantează codul fără erori; vă ajută doar să vă scrieți un cod mai bun care poate fi mai bine înțeles și modificat.
Chiar mai bine, dacă descoperiți un bug, este mult mai ușor să scrieți un test care reproduce bug-ul. În acest fel, puteți să vă asigurați că problema nu apare din nou - deoarece ați testat-o!
Puteți susține că acest proces nu este tehnic "TDD". Și ai dreptate! Acest exemplu este mai aproape de numărul programatorilor de zi cu zi. Dacă doriți un adevărat exemplu "TDD așa cum spui tu", lăsați un comentariu mai jos și intenționez să scriu unul în viitor.
Vă mulțumim pentru lectură!