Refactoring Legacy Code Del 5 - Spillets testbare metoder

Gammel kode. Ugyldig kode. Komplisert kode. Spaghetti kode. Gibberish tull. I to ord, Legacy Code. Dette er en serie som vil hjelpe deg med å jobbe og håndtere det.

I vår tidligere opplæring testet vi våre Runner-funksjoner. I denne leksjonen er det på tide å fortsette hvor vi sluttet med å teste våre Spill klasse. Nå, når du starter med en så stor del av kode som vi har her, er det fristende å begynne å teste i en topp-ned måte, metode etter metode. Dette er mesteparten av tiden umulig. Det er mye bedre å begynne å teste det med sine korte, testbare metoder. Dette er hva vi skal gjøre i denne leksjonen: finn og test disse metodene.

Opprette et spill

For å teste en klasse må vi initialisere et objekt av den aktuelle typen. Vi kan vurdere at vår første test er å skape et nytt objekt. Du vil bli overrasket over hvor mange hemmeligheter konstruktører kan skjule.

require_once __DIR__. '/ ... /trivia/php/Game.php'; klasse GameTest utvider PHPUnit_Framework_TestCase funksjon testWeCanCreateAGame () $ game = nytt spill (); 

Til vår overraskelse, Spill kan faktisk opprettes ganske enkelt. Ingen problemer mens du kjører bare nytt spill (). Ingenting bryter. Dette er en veldig god start, spesielt med tanke på det SpillKonstruktøren er ganske stor og det gjør mange ting.

Finne den første testbare metoden

Det er fristende å forenkle konstruktøren akkurat nå. Men vi har bare den gyldne mesteren for å sikre at vi ikke bryter noe. Før vi går til konstruktøren, må vi teste det meste av resten av klassen. Så, hvor skal vi starte?

Se etter den første metoden som returnerer en verdi og spør deg selv, "Kan jeg ringe og kontrollere verdien av denne metoden?". Hvis svaret er ja, er det en god kandidat til vår test.

funksjon isPlayable () $ minimumNumberOfPlayers = 2; returnere ($ this-> howManyPlayers ()> = $ minimumNumberOfPlayers); 

Hva med denne metoden? Det virker som en god kandidat. Bare to linjer, og den returnerer en boolsk verdi. Men vent, det kaller en annen metode, howManyPlayers ().

funksjon howManyPlayers () return count ($ this-> spillere); 

Dette er egentlig bare en metode som teller elementene i klassen ' spillere array. OK, så hvis vi ikke legger til noen spillere, burde det være null. isPlayable () skal returnere falsk. La oss se om vår antagelse er riktig.

funksjon testAJustCreatedNewGameIsNotPlayable () $ game = nytt spill (); $ Dette-> assertFalse ($ spill-> isPlayable ()); 

Vi omdøpte vår tidligere testmetode for å gjenspeile det vi virkelig vil teste. Da hevdet vi bare at spillet ikke kan spilles. Testen går forbi. Men falske positiver er vanlige i mange tilfeller. Derfor kan vi hevde sant og sørge for at testen mislykkes.

$ Dette-> assertTrue ($ spill-> isPlayable ());

Og det gjør det!

PHPUnit_Framework_ExpectationFailedException: Feilet hevdet at falsk er sant.

Så langt, ganske lovende. Vi klarte å teste metodenes innledende returverdi, verdien som er representert ved den første stat av Spill klasse. Vær oppmerksom på understreket ord: "state". Vi må finne en måte å kontrollere tilstanden til spillet. Vi må endre det, så det vil ha det minste antall spillere.

Hvis vi analyserer Spill's Legg til() metode, vil vi se at det legger til elementer i vårt utvalg.

array_push ($ this-> spillere, $ playerName);

Vår antagelse håndheves av måten den Legg til() Metoden brukes i RunnerFunctions.php.

funksjonsløp () $ aGame = nytt spill (); $ AGame-> legge til ( "Chet"); $ AGame-> legge til ( "Pat"); $ AGame-> legge til ( "Sue"); // ... //

Basert på disse observasjonene kan vi konkludere det ved å bruke Legg til() To ganger burde vi kunne ta med oss Spill inn i en stat med to spillere.

funksjon testAfterAddingTwoPlayersToANewGameItIsPlayable () $ game = nytt spill (); $ game-> add ('First Player'); $ game-> add ('Second Player'); $ Dette-> assertTrue ($ spill-> isPlayable ()); 

Ved å legge til denne andre testmetoden kan vi sikre isPlayable () returnerer sant, dersom forholdene er oppfylt.

Men du tror kanskje dette ikke er en enhetstest. Vi bruker Legg til() metode! Vi trener mer enn det minste kodenes minimum. Vi kunne i stedet bare legge til elementene til $ spillere array og ikke stole på Legg til() metode i det hele tatt.

Vel, svaret er ja og nei. Vi kunne gjøre det, fra et teknisk synspunkt. Det vil ha fordelen av direkte kontroll over arrayet. Det vil imidlertid ha ulempen med kodeduplisering mellom kode og tester. Så velg en av de dårlige alternativene du tror du kan leve med og bruke den. Jeg personlig foretrekker å gjenbruke metoder som Legg til().

Refactoring Tests

Vi er på grønt, vi refactor. Kan vi gjøre testene våre bedre? Vel ja, det kan vi. Vi kunne forvandle vår første test for å bekrefte alle forholdene for ikke nok spillere.

funksjonstestGame med ikkeEnoughPlayersIsNotPlayable () $ game = nytt spill (); $ Dette-> assertFalse ($ spill-> isPlayable ()); $ game-> add ('En spiller'); $ Dette-> assertFalse ($ spill-> isPlayable ()); 

Du har kanskje hørt om begrepet "Én påstand per test". Jeg er mest enig med det, men hvis du har en test som bekrefter et enkelt konsept og krever flere påstander om å gjøre bekreftelsen, tror jeg det er akseptabelt å bruke mer enn én påstand. Denne utsikten er også sterkt fremmet av Robert C. Martin i hans lære.

Men hva med vår andre testmetode? Er det bra nok? jeg sier nei.

$ game-> add ('First Player'); $ game-> add ('Second Player');

Disse to anropene plager meg litt. De er en detaljert implementering uten en eksplisitt forklaring i vår metode. Hvorfor ikke trekke dem ut i en privat metode?

funksjon testAfterAddingEnoughPlayersToANewGameItIsPlayable () $ game = nytt spill (); $ Dette-> addEnoughPlayers ($ spillet); $ Dette-> assertTrue ($ spill-> isPlayable ());  privat funksjon addEnoughPlayers ($ game) $ game-> add ('First Player'); $ game-> add ('Second Player'); 

Dette er mye bedre, og det fører oss også til et annet konsept som vi savnet. I begge testene uttrykte vi på en eller annen måte begrepet "nok spillere". Men hvor mye er nok? Er det to? Ja, for nå er det. Men vil vi at vår test mislykkes hvis Spilllogikk vil kreve minst tre spillere? Vi vil ikke at dette skal skje. Vi kan introdusere et offentlig statisk klassefelt for det.

klassespill statisk $ minimumNumberOfPlayers = 2; // ... // funksjon __construct () // ... // funksjon isPlayable () return ($ this-> howManyPlayers ()> = selv: $ minimumNumberOfPlayers);  // // //

Dette vil tillate oss å bruke det i våre tester.

privat funksjon addEnoughPlayers ($ game) for ($ i = 0; $ i < Game::$minimumNumberOfPlayers; $i++)  $game->legg til ('En spiller'); 

Vår lille hjelpemetode vil bare legge til spillere til nok er lagt til. Vi kan til og med lage en annen slik metode for vår første test, så vi legger til nesten nok spillere.

funksjonstestGame med ikkeEnoughPlayersIsNotPlayable () $ game = nytt spill (); $ Dette-> assertFalse ($ spill-> isPlayable ()); $ Dette-> addJustNothEnoughPlayers ($ spillet); $ Dette-> assertFalse ($ spill-> isPlayable ());  privat funksjon addJustNothEnoughPlayers ($ game) for ($ i = 0; $ i < Game::$minimumNumberOfPlayers - 1; $i++)  $game->legg til ('En spiller'); 

Men dette introduserte noen duplisering. Våre tohjelpsmetoder er ganske like. Kan vi ikke trekke ut en tredje fra dem?

privat funksjon addEnoughPlayers ($ game) $ this-> addManyPlayers ($ game, Game :: $ minimumNumberOfPlayers);  privat funksjon addJustNothEnoughPlayers ($ game) $ this-> addManyPlayers ($ game, Game :: $ minimumNumberOfPlayers - 1);  privat funksjon addManyPlayers ($ game, $ numberOfPlayers) for ($ i = 0; $ i < $numberOfPlayers; $i++)  $game->legg til ('En spiller'); 

Det er bedre, men det introduserer et annet problem. Vi reduserte duplisering i disse metodene, men vår $ spill objektet er nå gått ned tre nivåer. Det blir vanskelig å håndtere. Det er på tide å initialisere det i testens SETUP () metode og gjenbruk den.

klassen GameTest utvider PHPUnit_Framework_TestCase private $ game; funksjon setUp () $ this-> game = nytt spill;  funksjonstestagame med ingen avspillere () $ this-> assertFalse ($ this-> game-> isPlayable ()); $ Dette-> addJustNothEnoughPlayers (); $ Dette-> assertFalse ($ dette-> spill-> isPlayable ());  funksjonstestAfterAddingEnoughPlayersToANewGameItIsPlayable () $ this-> addEnoughPlayers ($ dette-> spillet); $ Dette-> assertTrue ($ dette-> spill-> isPlayable ());  privat funksjon addEnoughPlayers () $ this-> addManyPlayers (Game :: $ minimumNumberOfPlayers);  privat funksjon addJustNothEnoughPlayers () $ this-> addManyPlayers (Game :: $ minimumNumberOfPlayers - 1);  privat funksjon addManyPlayers ($ numberOfPlayers) for ($ i = 0; $ i < $numberOfPlayers; $i++)  $this->spill-> legg til ('en spiller'); 

Mye bedre. All irrelevant kode er i private metoder, $ spill er initialisert i SETUP () og mye forurensning ble fjernet fra testmetodene. Men vi måtte gjøre et kompromiss her. I vår første test begynner vi med en påstand. Dette antar det SETUP () vil alltid lage et tomt spill. Dette er greit for nå. Men på slutten av dagen må du innse at det ikke er noe slikt som perfekt kode. Det er bare kode med kompromisser som du er villig til å leve med.

Den andre testbare metoden

Hvis vi skanner vår Spill klasse fra toppen mot bunnen, er neste metode på vår liste Legg til(). Ja, den samme metoden vi brukte i våre tester i forrige avsnitt. Men kan vi teste det?

funksjon testItCanAddANewPlayer () $ this-> game-> add ('En spiller'); $ this-> assertEquals (1, count ($ this-> game-> players)); 

Nå er dette en annen måte å teste objekter på. Vi kaller vår metode og deretter verifiserer vi objektets tilstand. Som Legg til() returnerer alltid ekte, Det er ingen måte vi kan teste utdataene på. Men vi kan starte med en tom Spill objekt og kontroller om det er en enkelt bruker etter at vi har lagt til en. Men er det nok bekreftelse?

funksjon testItCanAddANewPlayer () $ this-> assertEquals (0, count ($ this-> game-> spillere)); $ this-> game-> add ('En spiller'); $ this-> assertEquals (1, count ($ this-> game-> players)); 

Ville det ikke vært bedre å også kontrollere om det ikke er noen spillere før vi ringer Legg til()? Vel, det kan være litt for mye her, men som du kan se i koden ovenfor, kan vi gjøre det. Og når du ikke er sikker på den opprinnelige tilstanden, bør du gjøre et påstand om det. Dette beskytter deg også mot fremtidige kodeendringer som kan endre objektets opprinnelige tilstand.

Men tester vi alle tingene Legg til() metoden gjør? Jeg sier nei. Foruten å legge til en bruker, stiller den også mange innstillinger for den. Vi bør også sjekke for dem.

funksjon testItCanAddANewPlayer () $ this-> assertEquals (0, count ($ this-> game-> spillere)); $ this-> game-> add ('En spiller'); $ this-> assertEquals (1, count ($ this-> game-> players)); $ this-> assertEquals (0, $ this-> game-> places [1]); $ this-> assertEquals (0, $ this-> game-> purses [1]); $ Dette-> assertFalse ($ dette-> spill-> inPenaltyBox [1]); 

Dette er bedre. Vi verifiserer hver handling som Legg til() metoden gjør det. Denne gangen foretrukket jeg å teste $ spillere array. Hvorfor? Vi kunne ha brukt howManyPlayers () metode som i utgangspunktet gjør det samme, ikke sant? Vel, i dette tilfellet anså vi at det er viktigere å beskrive våre påstander ved de effekter som Legg til() Metoden har på tilstanden til objektet. Hvis vi trenger å endre Legg til(), vi forventer at testen som tester sin strenge oppførsel, vil mislykkes. Jeg har hatt endeløse debatter med kolleger på Syneto om dette. Spesielt fordi denne typen test introduserer en sterk kobling mellom testen og hvordan Legg til() Metoden er faktisk implementert. Så, hvis du foretrekker å teste det omvendt, betyr det ikke at ideene dine er feil.

Vi kan trygt ignorere testingen av produksjonen, den echoln () linjer. De skriver bare innhold på skjermen. Vi ønsker ikke å røre disse metodene ennå. Vår gylne mester bygger helt på denne utgangen.

Refactoring Tests (Bis)

Vi har en annen testmetode med en ny passeringstest. Det er på tide å refactor begge, bare litt. La oss starte med våre tester. Er ikke de tre siste påstandene litt forvirrende? De ser ikke ut til å være relatert strengt til å legge til en spiller. La oss endre det:

funksjon testItCanAddANewPlayer () $ this-> assertEquals (0, count ($ this-> game-> spillere)); $ this-> game-> add ('En spiller'); $ this-> assertEquals (1, count ($ this-> game-> players)); $ Dette-> assertDefaultPlayerParametersAreSetFor (1); 

Det er bedre. Metoden er nå mer abstrakt, gjenbrukbar, uttrykkelig navngitt og skjuler alle de uvesentlige detaljene.

Refactoring the Legg til() Metode

Vi kan gjøre noe lignende med vår produksjonskode.

funksjon add ($ playerName) array_push ($ this-> spillere, $ playerName); $ Dette-> setDefaultPlayerParametersFor ($ dette-> howManyPlayers ()); echoln ($ playerName. "ble lagt til"); echoln ("De er spiller nummer". count ($ this-> spillere)); returnere sant; 

Vi hentet ut de uvesentlige detaljene i setDefaultPlayerParametersFor ().

privat funksjon setDefaultPlayerParametersFor ($ playerId) $ this-> plasserer [$ playerId] = 0; $ this-> purses [$ playerId] = 0; $ this-> inPenaltyBox [$ playerId] = false; 

Faktisk kom denne ideen til meg etter at jeg skrev testen. Dette er et annet fint eksempel på hvordan tester tvinger oss til å tenke på vår kode fra et annet synspunkt. Denne ulikheten på problemet, er det vi må utnytte og la våre tester veilede vårt design av produksjonskoden.

Den tredje testbare metoden

La oss finne vår tredje kandidat for testing. howManyPlayers () er for enkelt og indirekte allerede testet. rull() er for komplisert for å bli testet direkte. Pluss det returnerer null. stille spørsmål() ser ut til å være interessant ved første blikk, men det er alt presentasjon, ingen returverdi.

currentCategory () er testbar, men det er pent vanskelig å teste. Det er en stor velger med ti forhold. Vi trenger en ti linjers lang test, og vi må derfor reprodusere denne metoden og absolutt tester også. Vi bør ta oppmerksom på denne metoden og komme tilbake til den etter at vi er ferdig med de enklere. For oss vil dette være i vår neste opplæring.

wasCorrectlyAnswered () er for komplisert igjen. Vi må trekke ut fra det, små stykker kod som kan testes. derimot, feil svar() virker lovende. Det sender ut ting på skjermen, men det endrer også tilstanden til objektet vårt. La oss se om vi kan kontrollere det og teste det.

funksjonstestWhenAPlayerEntersAWrongAnswerItIsSentToThePenaltyBox () $ this-> game-> add ('En spiller'); $ this-> game-> currentPlayer = 0; $ Dette-> spill-> wrongAnswer (); $ Dette-> assertTrue ($ dette-> spill-> inPenaltyBox [0]); 

Grrr ... Det var ganske vanskelig å skrive denne testmetoden. feil svar() stoler på $ Dette-> currentPlayer for sin atferdsmessige logikk, men det bruker også $ Dette-> spillere i presentasjonsdelen. Et styggt eksempel på hvorfor du ikke bør blande logikk og presentasjon. Vi skal håndtere dette i en fremtidig opplæring. For nå har vi testet at brukeren kommer inn i straffefeltet. Vi må også observere at det er en hvis() uttalelse i metoden. Dette er en betingelse at vi ennå ikke test, da vi bare har en enkelt spiller og dermed ikke tilfredsstiller vi tilstanden. Vi kunne teste for den endelige verdien av $ currentPlayer selv om. Men å legge til denne linjen med koden til testen, får det til å feile.

$ this-> assertEquals (1, $ this-> game-> currentPlayer);

En nærmere titt på den private metoden shouldResetCurrentPlayer () avslører problemet. Hvis indeksen for gjeldende spiller er lik antall spillere, vil den bli nullstilt. Aaaahhh! Vi går faktisk inn i hvis()!

funksjonstestWhenAPlayerEntersAWrongAnswerItIsSentToThePenaltyBox () $ this-> game-> add ('En spiller'); $ this-> game-> currentPlayer = 0; $ Dette-> spill-> wrongAnswer (); $ Dette-> assertTrue ($ dette-> spill-> inPenaltyBox [0]); $ this-> assertEquals (0, $ this-> game-> currentPlayer);  funksjonstestCurrentPlayerIsNotResetAfterWrongAnswerIfOtherPlayersDidNotYetPlay () $ this-> addManyPlayers (2); $ this-> game-> currentPlayer = 0; $ Dette-> spill-> wrongAnswer (); $ this-> assertEquals (1, $ this-> game-> currentPlayer); 

Flink. Vi opprettet en ny test, for å teste det spesielle tilfellet når det fortsatt er spillere som ikke spilte. Vi bryr oss ikke om inPenaltyBox angi for den andre testen. Vi er bare interessert i indeksen til gjeldende spiller.

Den endelige testbare metoden

Den siste metoden vi kan teste og så refactor er didPlayerWin ().

funksjon didPlayerWin () $ numberOfCoinsToWin = 6; returnere! ($ this-> purses [$ this-> currentPlayer] == $ numberOfCoinsToWin); 

Vi kan umiddelbart observere at kodestrukturen er svært lik isPlayable (), metoden vi testet først. Vår løsning bør også være noe lignende. Når koden er så kort, er det bare to til tre linjer, og det gjør ikke mer enn ett lite skritt så stor risiko. I verste fall scenarier går du tilbake til tre linjer med kode. Så la oss gjøre dette i ett enkelt trinn.

funksjon testTestPlayerWinsWithTheCorrectNumberOfCoins () $ this-> game-> currentPlayer = 0; $ this-> game-> purses [0] = Spill :: $ numberOfCoinsToWin; $ Dette-> assertTrue ($ dette-> spill-> didPlayerWin ()); 

Men vent! Det mislykkes. Hvordan er det mulig? Skal det ikke passere? Vi ga riktig antall mynter. Hvis vi studerer vår metode, oppdager vi litt misvisende faktum.

returnere! ($ this-> purses [$ this-> currentPlayer] == $ numberOfCoinsToWin);

Avkastningsverdien er faktisk negert. Så metoden forteller oss ikke om en spiller vant, forteller den oss om en spiller ikke vant spillet. Vi kunne gå inn og finne stedene der denne metoden er brukt og negere verdien der. Deretter endrer du oppførselen her, for ikke å falle negativt svaret. Men det er brukt i wasCorrectlyAnswered (), en metode vi ikke kan enhets testen. Kanskje for tiden, en enkel omdøping for å markere riktig funksjonalitet vil være nok.

funksjon didPlayerNotWin () return! ($ this-> purses [$ this-> currentPlayer] == selv :: $ numberOfCoinsToWin); 

Tanker og konklusjon

Så dette handler om opplæringen. Mens vi ikke liker negasjonen i navnet, er dette et kompromiss vi kan gjøre på dette tidspunktet. Dette navnet vil sikkert endres når vi begynner å refactoring andre deler av koden. I tillegg, hvis du tar en titt på våre tester, ser de merkelig ut nå:

funksjon testTestPlayerWinsWithTheCorrectNumberOfCoins () $ this-> game-> currentPlayer = 0; $ this-> game-> purses [0] = Spill :: $ numberOfCoinsToWin; $ Dette-> assertFalse ($ dette-> spill-> didPlayerNotWin ()); 

Ved å teste feil på en negert metode, utøvet med en verdi som antyder et sant resultat, introduserte vi ganske mye forvirring til kodens lesbarhet. Men dette er bra for nå, da vi trenger å stoppe på et tidspunkt, rett?

I vår neste opplæring vil vi begynne å jobbe med noen av de vanskeligste metodene innen Spill klasse. Takk for at du leser.