Cod vechi. Cod urât. Cod complicat. Codul spaghetti. Gândurile proaste. Cu două cuvinte, Codul vechi. Aceasta este o serie care vă va ajuta să lucrați și să vă ocupați de aceasta.
În tutorialul nostru anterior, am testat funcțiile Runner. În această lecție, este timpul să continuăm unde am rămas prin testarea noastră Joc
clasă. Acum, când începeți cu o bucată mare de cod ca aici, este tentant să începeți să încercați într-o manieră de sus în jos, metodă prin metode. Aceasta este, de cele mai multe ori, imposibilă. Este mult mai bine să înceapă testul prin metodele sale scurte, verificabile. Aceasta este ceea ce vom face în această lecție: găsiți și testați aceste metode.
Pentru a testa o clasă trebuie să inițializăm un obiect de tipul respectiv. Putem considera că primul nostru test este acela de a crea un astfel de obiect nou. Veți fi surprins de cât de multe secrete constructorii se pot ascunde.
requ_once __DIR__. '/ ... / trivia/php/Game.php'; clasa GameTest extinde PHPUnit_Framework_TestCase function testWeCanCreateAGame () $ game = new Game ();
Spre surprinderea noastră, Joc
pot fi create cu ușurință. Nu există probleme în timp ce rulează doar joc nou()
. Nimic nu se rupe. Acesta este un început foarte bun, în special având în vedere acest lucru Joc
constructorul este destul de mare și face multe lucruri.
Este tentant să simplificăm constructorul chiar acum. Dar avem doar maestrul de aur pentru a ne asigura că nu vom rupe nimic. Înainte de a merge la constructor, trebuie să încercăm majoritatea restului clasei. Deci, unde ar trebui să începem?
Căutați prima metodă care returnează o valoare și întrebați-vă: "Pot să sun și să controlez valoarea returnată a acestei metode?". Dacă răspunsul este da, este un bun candidat pentru testul nostru.
este funcțiaPlayable () $ minimumNumberOfPlayers = 2; retur ($ this-> howManyPlayers ()> = $ minimNumberOfPlayers);
Cum rămâne cu această metodă? Se pare că este un bun candidat. Doar două linii și returnează o valoare booleană. Dar așteptați, se numește o altă metodă, howManyPlayers ()
.
funcția howManyPlayers () returnează numărul ($ this-> players);
Aceasta este în esență doar o metodă care numără elementele din clasa " jucători
matrice. OK, dacă nu adăugăm niciun jucător, ar trebui să fie zero. isPlayable ()
ar trebui să se întoarcă false. Să vedem dacă presupunerea noastră este corectă.
funcția testAJustCreatedNewGameIsNotPlayable () $ game = new Game (); $ This-> assertFalse ($ joc-> isPlayable ());
Am redenumit metoda noastră de testare anterioară pentru a reflecta ceea ce dorim cu adevărat să testați. Apoi am afirmat că jocul nu poate fi jucat. Testul trece. Dar fals pozitive sunt comune în multe cazuri. Deci, pentru o bucată de minte, putem afirma adevărat și ne asigurăm că testul nu reușește.
$ This-> assertTrue ($ joc-> isPlayable ());
Și asta se întâmplă!
PHPUnit_Framework_ExpectationFailedException: A apărut că False este adevărat.
Până acum, destul de promițătoare. Am reușit să testați valoarea returnată inițială a metodei, valoarea reprezentată de valoarea inițială stat din Joc
clasă. Vă rugăm să rețineți cuvântul subliniat: "state". Trebuie să găsim o modalitate de a controla stadiul jocului. Trebuie să-l schimbăm, așa că va avea numărul minim de jucători.
Dacă analizăm Joc
„s adăuga()
metodă, vom vedea că adaugă elemente la matricea noastră.
array_push ($ this-> players, $ playerName);
Presupunerea noastră este impusă de modul în care adăuga()
metoda este utilizată în RunnerFunctions.php
.
funcția execută () $ aGame = joc nou (); $ AGame-> adaugă ( "Chet"); $ AGame-> adaugă ( "Pat"); $ AGame-> add ( "Sue"); // ... // //
Pe baza acestor observații, putem concluziona că prin utilizarea adăuga()
de două ori, ar trebui să fim capabili să ne aducem Joc
într-o stare cu doi jucători.
funcția testAfterAddingTwoPlayersToANewGameItIsPlayable () $ game = new Game (); $ game-> add ("Primul jucător"); $ game-> add ("Jucător secund"); $ This-> assertTrue ($ joc-> isPlayable ());
Prin adăugarea acestei a doua metode de testare, putem asigura isPlayable ()
returnează adevărat, dacă condițiile sunt îndeplinite.
Dar ați putea crede că acest lucru nu este un test unic. Noi folosim adăuga()
metodă! Noi exercităm mai mult decât minimul minim de cod. În schimb, am putea adăuga elementele la $ jucători
matrice și nu se bazează pe adăuga()
metodă la toate.
Ei bine, răspunsul este da și nu. Am putea face acest lucru, din punct de vedere tehnic. Aceasta va avea avantajul unui control direct asupra matricei. Cu toate acestea, va avea dezavantajul duplicării codului între cod și teste. Deci, alegeți una dintre opțiunile rele pe care credeți că le puteți trăi și utilizați una. Eu personal prefer să refolosesc metode cum ar fi adăuga()
.
Suntem în verde, noi refactor. Putem face testele noastre mai bune? Da, putem. Am putea transforma primul nostru test pentru a verifica toate condițiile de jucători insuficienți.
funcția testAGameWithNotEnoughPlayersIsNotPlayable () $ game = new Game (); $ This-> assertFalse ($ joc-> isPlayable ()); $ game-> add ('Un jucător'); $ This-> assertFalse ($ joc-> isPlayable ());
Este posibil să fi auzit despre conceptul de "o afirmație pe test". Sunt mai mult de acord cu asta, dar dacă aveți un test care verifică un singur concept și necesită mai multe afirmații pentru a-și face verificarea, cred că este acceptabil să folosiți mai multe afirmații. Această viziune este, de asemenea, puternic promovată de Robert C. Martin în învățăturile sale.
Dar despre cea de-a doua metodă de testare? E destul de bun? Eu spun nu.
$ game-> add ("Primul jucător"); $ game-> add ("Jucător secund");
Aceste două apeluri mă deranjează puțin. Acestea sunt o implementare detaliată fără o explicație explicită în metoda noastră. De ce nu le extrageți într-o metodă privată?
funcția testAfterAddingEnoughPlayersToANewGameItIsPlayable () $ game = new Game (); $ This-> addEnoughPlayers ($ joc); $ This-> assertTrue ($ joc-> isPlayable ()); funcția privată addEnoughPlayers ($ joc) $ game-> add ("Primul jucător"); $ game-> add ("Jucător secund");
Acest lucru este mult mai bun și ne duce și la un alt concept pe care l-am pierdut. În ambele teste, am exprimat într-un fel sau altul conceptul de "destui jucători". Dar cât este de ajuns? Sunt doi? Da, pentru moment este. Dar dorim ca testul nostru să nu reușească în cazul în care Joc
logica va necesita cel puțin trei jucători? Nu vrem să se întâmple asta. Putem introduce un câmp public de clasă statică pentru el.
joc de clasă static $ minimumNumberOfPlayers = 2; // ... // funcția __construct () // ... // funcția isPlayable () return ($ this-> howManyPlayers ()> = auto :: $ minimumNumberOfPlayers); // ... //
Acest lucru ne va permite să ne folosim în testele noastre.
funcția privată addEnoughPlayers ($ joc) pentru ($ i = 0; $ i < Game::$minimumNumberOfPlayers; $i++) $game->adăugați ("Un jucător");
Mica noastră metodă de ajutor va adăuga jucători până când se adaugă suficient. Putem crea chiar o astfel de metodă pentru primul nostru test, așa că adăugăm aproape suficienți jucători.
funcția testAGameWithNotEnoughPlayersIsNotPlayable () $ game = new Game (); $ This-> assertFalse ($ joc-> isPlayable ()); $ This-> addJustNothEnoughPlayers ($ joc); $ This-> assertFalse ($ joc-> isPlayable ()); funcția privată addJustNothEnoughPlayers ($ game) pentru ($ i = 0; $ i < Game::$minimumNumberOfPlayers - 1; $i++) $game->adăugați ("Un jucător");
Dar aceasta a introdus o dublare. Cele două metode de ajutor sunt destul de asemănătoare. Nu putem extrage un al treilea dintre ele?
funcția privată addEnoughPlayers ($ joc) $ this-> addManyPlayers ($ joc, joc :: $ minimumNumberOfPlayers); funcția privată addJustNothEnoughPlayers ($ joc) $ this-> addManyPlayers ($ joc, joc :: $ minimumNumberOfPlayers - 1); funcția privată addManyPlayers ($ game, $ numberOfPlayers) pentru ($ i = 0; $ i < $numberOfPlayers; $i++) $game->adăugați ("Un jucător");
Asta e mai bine, dar introduce o altă problemă. Am redus duplicarea în aceste metode, dar noi $ joc
obiect este acum trecut în jos trei niveluri. Este dificil de gestionat. Este timpul să-l inițializăm în test înființat()
și reutilizați-l.
GameTest de clasă extinde PHPUnit_Framework_TestCase private $ game; funcția setUp () $ this-> game = joc nou; funcția testAGameWithNotEnoughPlayersIsNotPlayable () $ this-> assertFalse ($ this-> game-> isPlayable ()); $ This-> addJustNothEnoughPlayers (); $ This-> assertFalse ($ this-> joc-> isPlayable ()); funcția testAfterAddingEnoughPlayersToANewGameItIsPlayable () $ this-> addEnoughPlayers ($ this-> game); $ This-> assertTrue ($ this-> joc-> isPlayable ()); funcția privată addEnoughPlayers () $ this-> addManyPlayers (Joc :: $ minimumNumberOfPlayers); funcția privată addJustNothEnoughPlayers () $ this-> addManyPlayers (Joc :: $ minimumNumberOfPlayers - 1); funcția privată addManyPlayers ($ numberOfPlayers) pentru ($ i = 0; $ i < $numberOfPlayers; $i++) $this->joc-> adăugați ("Un jucător");
Mult mai bine. Codul irelevant este în metode private, $ joc
este inițializată în înființat()
și o mulțime de poluare a fost eliminată din metodele de testare. Cu toate acestea, a trebuit să facem un compromis aici. În primul nostru test, începem cu o afirmație. Acest lucru presupune asta înființat()
va crea întotdeauna un joc gol. Acest lucru este OK pentru acum. Dar, la sfârșitul zilei, trebuie să realizați că nu există un cod perfect. Există doar un cod cu compromisuri cu care sunteți dispus să trăiți.
Dacă ne scanăm Joc
clasa de sus în jos, următoarea metodă din lista noastră este adăuga()
. Da, aceeași metodă pe care am folosit-o în testele noastre din paragraful anterior. Dar o putem testa?
funcția testItCanAddANewPlayer () $ this-> game-> add ("Un jucător"); $ this-> assertEquals (1, numără ($ this-> game-> players));
Acum, acesta este un mod diferit de testare a obiectelor. Ne numim metoda și apoi verificăm starea obiectului. La fel de adăuga()
se întoarce întotdeauna Adevărat
, nu există nici o modalitate de a ne putea testa rezultatele. Dar putem începe cu un gol Joc
obiect și apoi verificați dacă există un singur utilizator după ce adăugăm unul. Dar este suficientă verificare?
funcția testItCanAddANewPlayer () $ this-> assertEquals (0, numără ($ this-> game-> players)); $ this-> game-> add ("Un jucător"); $ this-> assertEquals (1, numără ($ this-> game-> players));
Nu ar fi mai bine să verificăm dacă nu există jucători înainte de a ne apela adăuga()
? Ei bine, ar putea fi un pic prea mult aici, dar după cum puteți vedea în codul de mai sus, am putea să o facem. Și ori de câte ori nu sunteți sigur de starea inițială, trebuie să faceți o afirmație. Aceasta vă protejează de modificările viitoare ale codului care pot schimba starea inițială a obiectului.
Dar încercăm toate lucrurile adăuga()
metoda nu? Eu spun nu. Pe lângă adăugarea unui utilizator, acesta stabilește, de asemenea, o mulțime de setări pentru acesta. Ar trebui să verificăm și aceștia.
funcția testItCanAddANewPlayer () $ this-> assertEquals (0, numără ($ this-> game-> players)); $ this-> game-> add ("Un jucător"); $ this-> assertEquals (1, numără ($ this-> game-> players)); $ this-> assertEquals (0, $ acest-> joc-> locuri [1]); $ this-> assertEquals (0, $ this-> joc-> poșete [1]); $ This-> assertFalse ($ this-> joc-> inPenaltyBox [1]);
Asa este mai bine. Verificăm fiecare acțiune pe care o facem adăuga()
metoda face. De data aceasta, am preferat să testez direct $ jucători
matrice. De ce? Am fi putut folosi howManyPlayers ()
metoda care practic face același lucru, nu? Ei bine, în acest caz am considerat că este mai important să descriem afirmațiile noastre prin efectele pe care le are adăuga()
metoda are asupra stării obiectului. Dacă trebuie să ne schimbăm adăuga()
, ne-am aștepta ca testul care testează comportamentul său strict să nu reușească. Am avut dezbateri nesfârșite cu colegii mei de la Syneto despre asta. Mai ales pentru că acest tip de testare introduce o cuplare puternică între test și modul în care adăuga()
metoda este implementată efectiv. Deci, dacă preferați să o testați invers, nu înseamnă că ideile dvs. sunt greșite.
Putem ignora în siguranță testarea producției, echoln ()
linii. Ele transmit doar conținut pe ecran. Nu vrem să atingem aceste metode, totuși. Stăpânul nostru de aur se bazează total pe această ieșire.
Avem o altă metodă testată cu un nou test de trecere. E timpul să refaceți ambele, doar puțin. Să începem cu testele noastre. Nu sunt ultimele trei afirmații puțin confuz? Ele nu par a fi legate strict de adăugarea unui jucător. Să o schimbăm:
funcția testItCanAddANewPlayer () $ this-> assertEquals (0, numără ($ this-> game-> players)); $ this-> game-> add ("Un jucător"); $ this-> assertEquals (1, numără ($ this-> game-> players)); $ This-> assertDefaultPlayerParametersAreSetFor (1);
Asa e mai bine. Metoda este acum mai abstractă, reutilizabilă, numită în mod expres și ascunde toate detaliile neimportante.
adăuga()
MetodăPutem face ceva similar cu codul nostru de producție.
adăugați funcția ($ playerName) array_push ($ this-> players, $ playerName); $ This-> setDefaultPlayerParametersFor ($ this-> howManyPlayers ()); echoln ($ playerName. "a fost adăugat"); echoln ("Aceștia sunt numărul de jucători".) ($ this-> players)); return true;
Am extras detaliile neimportante setDefaultPlayerParametersFor ()
.
funcția privată setDefaultPlayerParametersFor ($ playerId) $ this-> places [$ playerId] = 0; $ this-> poșete [$ playerId] = 0; $ this-> înPenaltyBox [$ playerId] = false;
De fapt, această idee mi-a venit după ce am scris testul. Acesta este un alt exemplu frumos despre modul în care testele ne obligă să ne gândim la codul nostru dintr-un alt punct de vedere. Acest unghi diferit al problemei este ceea ce trebuie să exploatăm și să lăsăm testele noastre să ghideze proiectarea codului de producție.
Să găsim al treilea candidat pentru testare. howManyPlayers ()
este prea simplu și deja testat indirect. rola ()
este prea complex pentru a fi testat direct. În plus, se întoarce nul
. Întreabă întrebări()
pare a fi interesant la prima vedere, dar este tot de prezentare, nici o valoare de returnare.
currentCategory ()
este testabil, dar este destul dificil a testa. Este un selector enorm cu zece condiții. Avem nevoie de un test de zece linii și apoi trebuie să refacem serios această metodă și, cu siguranță, și testele. Ar trebui să luăm notă de această metodă și să ne întoarcem după ce am terminat cu cele mai ușoare. Pentru noi, acest lucru va fi în următorul nostru tutorial.
wasCorrectlyAnswered ()
este de a complica din nou. Va trebui să extragem din ea, mici bucăți de cod care sunt testabile. in orice caz, răspuns greșit()
pare promițătoare. Emite lucruri pe ecran, dar de asemenea modifică starea obiectului nostru. Să vedem dacă o putem controla și o putem testa.
funcția testWhenAPlayerEntersAWrongAnswerItIsSentToThePenaltyBox () $ this-> game-> add ("Un jucător"); $ this-> game-> currentPlayer = 0; $ This-> joc-> wrongAnswer (); $ This-> assertTrue ($ this-> joc-> inPenaltyBox [0]);
Grrr ... A fost greu să scriem această metodă de testare. răspuns greșit()
se bazează pe $ This-> currentPlayer
pentru logica comportamentală, dar de asemenea folosește $ This-> jucători
în partea de prezentare. Un exemplu urât de ce nu ar trebui să amestecați logica și prezentarea. Vom aborda acest lucru într-un viitor tutorial. Pentru moment, am testat că utilizatorul intră în caseta de penalizare. Trebuie să observăm, de asemenea, că există dacă()
declarație în metodă. Aceasta este o condiție pe care nu o testăm încă, deoarece avem doar un singur jucător și, prin urmare, nu satisfacem această condiție. Am putea testa pentru valoarea finală a $ currentPlayer
deşi. Dar adăugarea acestei linii de cod la test va face să eșueze.
$ this-> assertEquals (1, $ acest-> joc-> curentPlayer);
O privire mai atentă la metoda privată shouldResetCurrentPlayer ()
dezvăluie problema. Dacă indicele jucătorului curent este egal cu numărul de jucători, acesta va fi resetat la zero. Aaaahhh! Intrăm de fapt dacă()
!
funcția testWhenAPlayerEntersAWrongAnswerItIsSentToThePenaltyBox () $ this-> game-> add ("Un jucător"); $ this-> game-> currentPlayer = 0; $ This-> joc-> wrongAnswer (); $ This-> assertTrue ($ this-> joc-> inPenaltyBox [0]); $ this-> assertEquals (0, $ acest-> joc-> curentPlayer); funcția testCurrentPlayerIsNotResetAfterWrongAnswerIfOtherPlayersDidNotYetPlay () $ this-> addManyPlayers (2); $ this-> game-> currentPlayer = 0; $ This-> joc-> wrongAnswer (); $ this-> assertEquals (1, $ acest-> joc-> curentPlayer);
Bun. Am creat un al doilea test, pentru a testa cazul specific atunci când încă mai există jucători care nu au jucat. Nu ne pasă de inPenaltyBox
pentru al doilea test. Suntem interesați doar de indicele actualului jucător.
Ultima metodă pe care o putem testa și apoi refactor este didPlayerWin ()
.
funcția didPlayerWin () $ numberOfCoinsToWin = 6; reveniți! ($ this-> purses [$ this-> currentPlayer] == $ numberOfCoinsToWin);
Putem observa imediat că structura codului său este foarte asemănătoare isPlayable ()
, metoda pe care am testat-o mai intai. Soluția noastră ar trebui să fie și ceva similar. Când codul dvs. este atât de scurt, doar două până la trei linii, care fac mai mult decât un pas mic, nu sunt atât de mari de risc. În cele mai grave scenarii, reveniți la trei rânduri de cod. Deci, să facem acest lucru într-un singur pas.
funcția testTestPlayerWinsWithTheCorrectNumberOfCoins () $ this-> game-> currentPlayer = 0; $ this-> game-> purses [0] = Joc :: $ numberOfCoinsToWin; $ This-> assertTrue ($ this-> joc-> didPlayerWin ());
Dar asteapta! Asta nu reușește. Cum este posibil? Nu trebuia să treacă? Am furnizat numărul corect de monede. Dacă studiem metoda noastră, vom descoperi un mic fapt înșelător.
reveniți! ($ this-> purses [$ this-> currentPlayer] == $ numberOfCoinsToWin);
Valoarea returnată este de fapt negată. Deci, metoda nu ne spune dacă un jucător a câștigat, ne spune dacă un jucător nu a câștigat jocul. Am putea să intrăm și să găsim locurile în care această metodă este folosită și să negăm valoarea acesteia acolo. Apoi schimbați comportamentul său aici, pentru a nu neglija în mod fals răspunsul. Dar este folosit în wasCorrectlyAnswered ()
, o metodă pe care nu o putem încă încerca unitatea. Poate pentru moment, o simplă redenumire pentru a evidenția funcționalitatea corectă va fi de ajuns.
funcția didPlayerNotWin () retur! ($ this-> purses [$ this-> currentPlayer] == auto :: $ numberOfCoinsToWin);
Deci, acest lucru despre wraps sus tutorial. Deși nu ne place negația din nume, acesta este un compromis pe care îl putem face în acest moment. Acest nume se va schimba, cu siguranță, atunci când vom începe să refactorizăm alte părți ale codului. În plus, dacă analizați testele noastre, ele arată ciudate acum:
funcția testTestPlayerWinsWithTheCorrectNumberOfCoins () $ this-> game-> currentPlayer = 0; $ this-> game-> purses [0] = Joc :: $ numberOfCoinsToWin; $ This-> assertFalse ($ this-> joc-> didPlayerNotWin ());
Prin testarea falsă pe o metodă negată, exercitată cu o valoare care sugerează un adevărat rezultat, am introdus destul de multă confuzie pentru citirea codurilor noastre. Dar este bine pentru moment, pentru că trebuie să ne oprim la un moment dat, corect?
În următorul tutorial, vom începe să lucrăm la unele dintre metodele mai dificile din cadrul Joc
clasă. Mulțumesc că ați citit.