Nacalculatie van oude code deel 5 - Testbare methoden van het spel

Oude code. Lelijke code. Ingewikkelde code. Spaghetti-code. Gibberistische onzin. In twee woorden, Oude code. Dit is een serie die u zal helpen om ermee te werken en ermee om te gaan.

In onze vorige zelfstudie hebben we onze Runner-functies getest. In deze les is het tijd om verder te gaan waar we zijn gebleven door onze testen Spel klasse. Nu, wanneer u begint met zo'n groot stuk code zoals we hier hebben, is het verleidelijk om van boven naar beneden, methode per methode, te beginnen met testen. Dit is meestal onmogelijk. Het is veel beter om het te testen met zijn korte, testbare methoden. Dit is wat we in deze les zullen doen: die methoden zoeken en testen.

Een spel maken

Om een ​​klasse te testen, moeten we een object van dat specifieke type initialiseren. We kunnen aannemen dat onze eerste test is om zo'n nieuw object te maken. Je zult verrast zijn hoeveel geheimen constructeurs kunnen verbergen.

require_once __DIR__. '/ ... /trivia/php/Game.php'; class GameTest breidt PHPUnit_Framework_TestCase uit function testWeCanCreateAGame () $ game = new Game (); 

Tot onze verbazing, Spel kan eigenlijk vrij gemakkelijk worden gemaakt. Geen problemen tijdens het hardlopen nieuw spel(). Niets breekt. Dit is een zeer goed begin, vooral gezien dat Spel's constructor is vrij groot en het doet een heleboel dingen.

De eerste testbare methode vinden

Het is verleidelijk om de constructor nu te vereenvoudigen. Maar we hebben alleen de gouden meester om ervoor te zorgen dat we niets breken. Voordat we naar de constructor gaan, moeten we het grootste deel van de rest van de klas testen. Waar moeten we beginnen??

Zoek naar de eerste methode die een waarde retourneert en stel jezelf de vraag: "Kan ik de retourwaarde van deze methode bellen en beheren?". Als het antwoord ja is, is het een goede kandidaat voor onze test.

function isPlayable () $ minimumNumberOfPlayers = 2; return ($ this-> howManyPlayers ()> = $ minimumNumberOfPlayers); 

Hoe zit het met deze methode? Het lijkt een goede kandidaat. Slechts twee regels en er wordt een Booleaanse waarde geretourneerd. Maar wacht, het roept een andere methode op, hoe veel spelers().

function howManyPlayers () return count ($ this-> players); 

Dit is eigenlijk gewoon een methode die de elementen in de klas telt ' spelers matrix. OK, dus als we geen spelers toevoegen, zou het nul moeten zijn. isPlayable () zou false moeten retourneren. Laten we kijken of onze aanname correct is.

function testAJustCreatedNewGameIsNotPlayable () $ game = new Game (); $ This-> assertFalse ($ Spel-> isPlayable ()); 

We hebben onze vorige testmethode hernoemd om weer te geven wat we echt willen testen. Toen beweerden we dat de game niet kan worden gespeeld. De test gaat voorbij. Maar valse positieven komen vaak voor in veel gevallen. Dus voor de gemoedsrust kunnen we waar maken en ervoor zorgen dat de test faalt.

$ This-> assertTrue ($ Spel-> isPlayable ());

En dat doet het!

PHPUnit_Framework_ExpectationFailedException: mislukt met beweren dat false waar is.

Tot nu toe redelijk belovend. We zijn erin geslaagd om de initiële retourwaarde van de methode te testen, de waarde die wordt voorgesteld door de initiaal staat van de Spel klasse. Let op het benadrukte woord: "state". We moeten een manier vinden om de status van het spel te controleren. We moeten het veranderen, dus het heeft het minimum aantal spelers.

Als we analyseren Spel's toevoegen() methode, we zullen zien dat het elementen aan onze array toevoegt.

array_push ($ dit-> spelers, $ playerName);

Onze aanname wordt afgedwongen door de manier waarop het toevoegen() methode wordt gebruikt in RunnerFunctions.php.

function run () $ aGame = nieuw spel (); $ AGame-> toe te voegen ( "Chet"); $ AGame-> toe te voegen ( "Pat"); $ AGame-> toe te voegen ( "Sue"); // ... //

Op basis van deze waarnemingen kunnen we concluderen dat door het gebruik van toevoegen() twee keer zouden we onze Spel in een staat met twee spelers.

function testAfterAddingTwoPlayersToANewGameItIsPlayable () $ game = new Game (); $ game-> add ('Eerste speler'); $ game-> add ('Second Player'); $ This-> assertTrue ($ Spel-> isPlayable ()); 

Door deze tweede testmethode toe te voegen, kunnen we ervoor zorgen isPlayable () geeft als resultaat waar, als aan de voorwaarden is voldaan.

Maar misschien denk je dat dit niet echt een test is. Wij gebruiken de toevoegen() methode! We oefenen meer uit dan het absolute minimum van code. In plaats daarvan kunnen we de elementen toevoegen aan de $ spelers array en vertrouw niet op de toevoegen() methode helemaal.

Welnu, het antwoord is ja en nee. We kunnen dat doen, vanuit een technisch oogpunt. Het heeft het voordeel van directe controle over de array. Het heeft echter het nadeel van codeduplicatie tussen code en tests. Kies dus een van de slechte opties waarvan je denkt dat je ermee kunt leven en gebruik die. Persoonlijk geef ik er de voorkeur aan methoden te gebruiken zoals toevoegen().

Testen refactoren

We zijn groen, we refactoren. Kunnen we onze tests verbeteren? Nou ja, dat kunnen we. We zouden onze eerste test kunnen transformeren om alle voorwaarden van niet genoeg spelers te verifiëren.

function testAGameWithNotEnoughPlayersIsNotPlayable () $ game = new Game (); $ This-> assertFalse ($ Spel-> isPlayable ()); $ game-> add ('Een speler'); $ This-> assertFalse ($ Spel-> isPlayable ()); 

U hebt misschien wel eens gehoord van het concept 'Eén bewering per test'. Ik ben het daar meestal mee eens, maar als je een test hebt die een enkel concept verifieert en meerdere beweringen vereist om de verificatie uit te voeren, denk ik dat het acceptabel is om meer dan één bewering te gebruiken. Deze visie wordt ook sterk gepromoot door Robert C. Martin in zijn lessen.

Maar hoe zit het met onze tweede testmethode? Is dat goed genoeg? ik zeg nee.

$ game-> add ('Eerste speler'); $ game-> add ('Second Player');

Deze twee telefoontjes storen me een beetje. Ze zijn een gedetailleerde implementatie zonder een expliciete verklaring in onze methode. Waarom niet extraheren naar een privémethode?

function testAfterAddingEnoughPlayersToANewGameItIsPlayable () $ game = new Game (); $ This-> addEnoughPlayers ($ spel); $ This-> assertTrue ($ Spel-> isPlayable ());  private functie addEnoughPlayers ($ game) $ game-> add ('Eerste speler'); $ game-> add ('Second Player'); 

Dit is veel beter en het leidt ons ook naar een ander concept dat we hebben gemist. In beide tests hebben we op de een of andere manier het concept van "genoeg spelers" uitgedrukt. Maar hoeveel is genoeg? Is het twee? Ja, voor nu is het dat. Maar willen we dat onze test mislukt als de Spelzijn logica vereist minstens drie spelers? We willen niet dat dit gebeurt. We kunnen er een openbaar static class-veld voor introduceren.

class Game static $ minimumNumberOfPlayers = 2; // ... // functie __construct () // ... // functie isPlayable () return ($ this-> howManyPlayers ()> = self :: $ minimumNumberOfPlayers);  // ... //

Dit zal ons in staat stellen om het te gebruiken in onze tests.

private functie addEnoughPlayers ($ game) for ($ i = 0; $ i < Game::$minimumNumberOfPlayers; $i++)  $game->toevoegen ('A Player'); 

Onze kleine hulpmethode voegt spelers toe totdat er genoeg is toegevoegd. We kunnen zelfs een andere dergelijke methode voor onze eerste test maken, dus voegen we bijna genoeg spelers toe.

function testAGameWithNotEnoughPlayersIsNotPlayable () $ game = new Game (); $ This-> assertFalse ($ Spel-> isPlayable ()); $ This-> addJustNothEnoughPlayers ($ spel); $ This-> assertFalse ($ Spel-> isPlayable ());  private functie addJustNothEnoughPlayers ($ game) for ($ i = 0; $ i < Game::$minimumNumberOfPlayers - 1; $i++)  $game->voeg toe ('Een speler'); 

Maar dit introduceerde wat duplicatie. Onze twee hulpmethoden zijn redelijk vergelijkbaar. Kunnen we er geen derde uithalen??

private functie addEnoughPlayers ($ game) $ this-> addManyPlayers ($ game, Game :: $ minimumNumberOfPlayers);  private functie addJustNothEnoughPlayers ($ game) $ this-> addManyPlayers ($ game, Game :: $ minimumNumberOfPlayers - 1);  private functie addManyPlayers ($ game, $ numberOfPlayers) for ($ i = 0; $ i < $numberOfPlayers; $i++)  $game->toevoegen ('A Player'); 

Dat is beter, maar het introduceert een ander probleem. We hebben duplicatie in deze methoden verminderd, maar onze $ spel object wordt nu op drie niveaus doorgegeven. Het wordt moeilijk om te beheren. Het is tijd om het in de test te initialiseren opstelling() methode en hergebruik.

class GameTest breidt PHPUnit_Framework_TestCase uit private $ game; functie setUp () $ this-> game = nieuwe game;  function testAGameWithNotEnoughPlayersIsNotPlayable () $ this-> assertFalse ($ this-> game-> isPlayable ()); $ This-> addJustNothEnoughPlayers (); $ This-> assertFalse ($ this-> Spel-> isPlayable ());  function testAfterAddingEnoughPlayersToANewGameItIsPlayable () $ this-> addEnoughPlayers ($ this-> game); $ This-> assertTrue ($ this-> Spel-> isPlayable ());  private function addEnoughPlayers () $ this-> addManyPlayers (Game :: $ minimumNumberOfPlayers);  private functie addJustNothEnoughPlayers () $ this-> addManyPlayers (Game :: $ minimumNumberOfPlayers - 1);  private function addManyPlayers ($ numberOfPlayers) for ($ i = 0; $ i < $numberOfPlayers; $i++)  $this->game-> add ('A Player'); 

Veel beter. Alle irrelevante code is in privé-methoden, $ spel is geïnitialiseerd in opstelling() en veel vervuiling werd verwijderd uit de testmethoden. We moesten hier echter een compromis sluiten. In onze eerste test beginnen we met een bewering. Dit veronderstelt dat opstelling() zal altijd een leeg spel maken. Dit is OK voor nu. Maar aan het eind van de dag moet u zich realiseren dat er niet zoiets bestaat als perfecte code. Er is gewoon code met compromissen waarmee je bereid bent om mee te leven.

De tweede testbare methode

Als we onze scannen Spel Klasse van boven naar beneden, de volgende methode op onze lijst is toevoegen(). Ja, dezelfde methode die we in onze tests in de vorige paragraaf hebben gebruikt. Maar kunnen we het testen?

function testItCanAddANewPlayer () $ this-> game-> add ('Een speler'); $ this-> assertEquals (1, count ($ this-> game-> spelers)); 

Dit is een andere manier om objecten te testen. We noemen onze methode en vervolgens controleren we de staat van het object. Zoals toevoegen() keert altijd terug waar, er is geen manier om de output te testen. Maar we kunnen beginnen met een lege Spel object en controleer vervolgens of er één gebruiker is nadat we er één hebben toegevoegd. Maar is dat voldoende verificatie?

function testItCanAddANewPlayer () $ this-> assertEquals (0, count ($ this-> game-> players)); $ this-> game-> add ('Een speler'); $ this-> assertEquals (1, count ($ this-> game-> spelers)); 

Zou het niet beter zijn om ook te verifiëren of er geen spelers zijn voordat we bellen toevoegen()? Nou, het is hier misschien een beetje te veel, maar zoals je kunt zien in de bovenstaande code, kunnen we het doen. En wanneer u niet zeker bent van de oorspronkelijke toestand, moet u er een bewering over doen. Dit beschermt u ook tegen toekomstige codewijzigingen die de beginstatus van uw object kunnen veranderen.

Maar testen we alle dingen die de toevoegen() methode doet? Ik zeg nee. Naast het toevoegen van een gebruiker, worden er ook veel instellingen voor ingesteld. Daar moeten we ook naar kijken.

function testItCanAddANewPlayer () $ this-> assertEquals (0, count ($ this-> game-> players)); $ this-> game-> add ('Een speler'); $ this-> assertEquals (1, count ($ this-> game-> spelers)); $ this-> assertEquals (0, $ this-> game-> places [1]); $ this-> assertEquals (0, $ this-> game-> portemonnees [1]); $ This-> assertFalse ($ this-> Spel-> inPenaltyBox [1]); 

Dit is beter. We verifiëren elke actie die de toevoegen() methode doet. Deze keer heb ik de voorkeur gegeven aan het direct testen van de $ spelers matrix. Waarom? We hadden de hoe veel spelers() methode die eigenlijk hetzelfde doet, toch? Nou, in dit geval vonden we het belangrijker om onze beweringen te beschrijven door de effecten die de toevoegen() methode heeft de status van het object. Als we moeten veranderen toevoegen(), we zouden verwachten dat de test die zijn strikt gedrag test, zal mislukken. Ik heb hierover eindeloze discussies gevoerd met mijn collega's in Syneto. Vooral omdat dit type testen een sterke koppeling introduceert tussen de test en hoe het toevoegen() methode is daadwerkelijk geïmplementeerd. Dus als je het liever andersom wilt testen, betekent dat niet dat je ideeën verkeerd zijn.

We kunnen het testen van de uitvoer veilig negeren, de echoln () lijnen. Ze voeren gewoon inhoud op het scherm uit. We willen deze methoden nog niet aanraken. Onze gouden meester vertrouwt volledig op deze output.

Testen van refactoren (Bis)

We hebben een andere geteste methode met een geheel nieuwe passingstest. Het is tijd om beide te refactiveren, een klein beetje. Laten we beginnen met onze tests. Zijn de laatste drie beweringen niet een beetje verwarrend? Ze lijken niet strikt gerelateerd te zijn aan het toevoegen van een speler. Laten we het veranderen:

function testItCanAddANewPlayer () $ this-> assertEquals (0, count ($ this-> game-> players)); $ this-> game-> add ('Een speler'); $ this-> assertEquals (1, count ($ this-> game-> spelers)); $ This-> assertDefaultPlayerParametersAreSetFor (1); 

Dat is beter. De methode is nu meer abstract, herbruikbaar, expressief benoemd en verbergt alle onbelangrijke details.

Het refactoring van toevoegen() Methode

We kunnen iets soortgelijks doen met onze productiecode.

functie add ($ playerName) array_push ($ this-> spelers, $ playerName); $ This-> setDefaultPlayerParametersFor ($ this-> howManyPlayers ()); echoln ($ playerName. "is toegevoegd"); echoln ("Ze zijn speler nummer". count ($ dit-> spelers)); geef waar terug; 

We hebben de onbelangrijke details eruit gehaald setDefaultPlayerParametersFor ().

persoonlijke functie setDefaultPlayerParametersFor ($ playerId) $ this-> places [$ playerId] = 0; $ this-> portemonnees [$ playerId] = 0; $ this-> inPenaltyBox [$ playerId] = false; 

Eigenlijk kwam dit idee bij mij nadat ik de test had geschreven. Dit is een ander mooi voorbeeld van hoe tests ons dwingen om vanuit een ander gezichtspunt over onze code na te denken. Deze verschillende invalshoek van het probleem is wat we moeten benutten en onze tests moeten ons ontwerp van de productiecode leiden.

De derde testbare methode

Laten we onze derde kandidaat voor testen vinden. hoe veel spelers() is te simpel en indirect al getest. rollen() is te complex om direct te worden getest. En het komt terug nul. vragen stellen() lijkt op het eerste gezicht interessant, maar het is allemaal presentatie, geen retourwaarde.

currentCategory () is toetsbaar, maar het is mooi moeilijk testen. Het is een enorme selector met tien voorwaarden. We hebben een tien-lijnen-lange test nodig en dan moeten we deze methode en zeker de tests serieus refactoren. We moeten nota nemen van deze methode en er later op terugkomen nadat we klaar zijn met de gemakkelijkere. Voor ons zal dit in onze volgende tutorial zijn.

wasCorrectlyAnswered () is weer ingewikkeld. We zullen er kleine stukjes code uit moeten halen die kunnen worden getest. Echter, wrongAnswer () lijkt veelbelovend. Het geeft dingen weer op het scherm, maar het verandert ook de status van ons object. Laten we kijken of we het kunnen beheersen en testen.

function testWanneerAPlayerEntersAWrongAnswerItIsSentToThePenaltyBox () $ this-> game-> add ('A player'); $ this-> game-> currentPlayer = 0; $ This-> Spel-> wrongAnswer (); $ This-> assertTrue ($ this-> Spel-> inPenaltyBox [0]); 

Grrr ... Het was vrij moeilijk om deze testmethode te schrijven. wrongAnswer () vertrouwt op $ This-> currentPlayer voor zijn gedragslogica, maar het gebruikt ook $ This-> spelers in zijn presentatiedeel. Een lelijk voorbeeld van waarom je logica en presentatie niet zou moeten combineren. We zullen dit in een toekomstige tutorial behandelen. Voorlopig hebben we getest dat de gebruiker de strafschopsteen invoert. We moeten ook vaststellen dat er een is als() verklaring in de methode. Dit is een voorwaarde die we nog niet testen, omdat we maar één speler hebben en we dus niet aan de voorwaarde voldoen. We kunnen testen voor de uiteindelijke waarde van $ currentPlayer though. Maar als u deze regel code aan de test toevoegt, mislukt deze.

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

De privémethode van naderbij bekijken shouldResetCurrentPlayer () onthult het probleem. Als de index van de huidige speler gelijk is aan het aantal spelers, wordt deze teruggezet naar nul. Aaaahhh! We komen eigenlijk in de als()!

function testWanneerAPlayerEntersAWrongAnswerItIsSentToThePenaltyBox () $ this-> game-> add ('A player'); $ this-> game-> currentPlayer = 0; $ This-> Spel-> wrongAnswer (); $ This-> assertTrue ($ this-> Spel-> inPenaltyBox [0]); $ this-> assertEquals (0, $ this-> game-> currentPlayer);  function testCurrentPlayerIsNotResetAfterWrongAnswerIfOtherPlayersDidNotYetPlay () $ this-> addManyPlayers (2); $ this-> game-> currentPlayer = 0; $ This-> Spel-> wrongAnswer (); $ this-> assertEquals (1, $ this-> game-> currentPlayer); 

Goed. We hebben een tweede test gemaakt om het specifieke geval te testen wanneer er nog steeds spelers zijn die niet hebben gespeeld. We geven niet om de inPenaltyBox staat voor de tweede test. We zijn alleen geïnteresseerd in de index van de huidige speler.

De laatste testbare methode

De laatste methode die we kunnen testen en vervolgens refactoren is didPlayerWin ().

function didPlayerWin () $ numberOfCoinsToWin = 6; return! ($ this-> portemonnees [$ this-> currentPlayer] == $ numberOfCoinsToWin); 

We kunnen meteen constateren dat de codestructuur ervan sterk lijkt op isPlayable (), de methode die we eerst hebben getest. Onze oplossing zou ook iets vergelijkbaars moeten zijn. Wanneer je code zo kort is, zijn slechts twee tot drie regels, meer dan een kleine stap doen, niet zo'n groot risico. In de worstcasescenario's keert u drie regels code terug. Dus laten we dit in één stap doen.

function testTestPlayerWinsWithTheCorrectNumberOfCoins () $ this-> game-> currentPlayer = 0; $ this-> game-> portemonnees [0] = Game :: $ numberOfCoinsToWin; $ This-> assertTrue ($ this-> Spel-> didPlayerWin ()); 

Maar wacht! Dat mislukt. Hoe is dat mogelijk? Moet het niet doorgaan? We hebben het juiste aantal munten opgegeven. Als we onze methode bestuderen, ontdekken we een beetje een misleidend feit.

return! ($ this-> portemonnees [$ this-> currentPlayer] == $ numberOfCoinsToWin);

De geretourneerde waarde wordt eigenlijk genegeerd. Dus de methode vertelt ons niet of een speler heeft gewonnen, het vertelt ons of een speler het spel niet heeft gewonnen. We kunnen naar binnen gaan en de plaatsen vinden waar deze methode wordt gebruikt en daar de waarde ervan tenietdoen. Verander dan zijn gedrag hier, om het antwoord niet valselijk te ontkennen. Maar het wordt gebruikt in wasCorrectlyAnswered (), een methode die we nog niet kunnen testen. Misschien is het voorlopig voldoende om de juiste functionaliteit eenvoudigweg te hernoemen.

function didPlayerNotWin () return! ($ this-> portemonnees [$ this-> currentPlayer] == self :: $ numberOfCoinsToWin); 

Gedachten en conclusie

Dus dit gaat over de tutorial. Hoewel we de negatie in de naam niet leuk vinden, is dit een compromis dat we op dit punt kunnen bereiken. Deze naam zal zeker veranderen wanneer we beginnen met het herschrijven van andere delen van de code. Bovendien, als je onze tests bekijkt, zien ze er vreemd uit:

function testTestPlayerWinsWithTheCorrectNumberOfCoins () $ this-> game-> currentPlayer = 0; $ this-> game-> portemonnees [0] = Game :: $ numberOfCoinsToWin; $ This-> assertFalse ($ this-> Spel-> didPlayerNotWin ()); 

Door vals te testen op een ontkende methode, uitgeoefend met een waarde die een echt resultaat suggereert, introduceerden we nogal wat verwarring over de leesbaarheid van onze codes. Maar dit is goed voor nu, omdat we op een gegeven moment toch moeten stoppen?

In onze volgende tutorial zullen we beginnen met het werken aan enkele van de moeilijkere methoden binnen de Spel klasse. Bedankt voor het lezen.