Überarbeitung des Legacy-Codes Teil 5 - Testbare Methoden des Spiels

Alter Code. Hässlicher Code. Komplizierter Code Spaghetti-Code. Gibberish Blödsinn. In zwei Worten, Legacy Code. Diese Serie hilft Ihnen bei der Arbeit und beim Umgang damit.

In unserem vorherigen Tutorial haben wir unsere Runner-Funktionen getestet. In dieser Lektion ist es an der Zeit, dort weiterzumachen, wo wir aufgehört haben Spiel Klasse. Wenn Sie nun mit einem so großen Code-Block wie hier beginnen, ist es verlockend, einen Test von oben nach unten zu beginnen, Methode für Methode. Dies ist meistens unmöglich. Es ist viel besser, mit den kurzen testbaren Methoden zu testen. Dies tun wir in dieser Lektion: Finden und testen Sie diese Methoden.

Ein Spiel erstellen

Um eine Klasse zu testen, müssen wir ein Objekt dieses bestimmten Typs initialisieren. Wir können davon ausgehen, dass unser erster Test das Erstellen eines solchen neuen Objekts ist. Sie werden überrascht sein, wie viele Geheimnisse Konstrukteure verstecken können.

required_once __DIR__. '/… /Trivia/php/Game.php'; Klasse GameTest erweitert PHPUnit_Framework_TestCase function testWeCanCreateAGame () $ game = new Game (); 

Zu unserer Überraschung, Spiel kann eigentlich ganz einfach erstellt werden. Keine Probleme beim Laufen einfach neues Spiel(). Nichts geht kaputt. Dies ist ein sehr guter Anfang, vor allem wenn man das bedenkt Spiel's Konstruktor ist ziemlich groß und macht viele Dinge.

Die erste testbare Methode finden

Es ist verlockend, den Konstruktor jetzt zu vereinfachen. Aber wir haben nur den goldenen Meister, um sicherzustellen, dass wir nichts kaputt machen. Bevor wir zum Konstruktor gehen, müssen wir den größten Teil der Klasse testen. Wo sollen wir anfangen??

Suchen Sie nach der ersten Methode, die einen Wert zurückgibt, und fragen Sie sich: "Kann ich den Rückgabewert dieser Methode aufrufen und steuern?". Wenn die Antwort "Ja" lautet, ist dies ein guter Kandidat für unseren Test.

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

Was ist mit dieser Methode? Es scheint ein guter Kandidat zu sein. Nur zwei Zeilen und es gibt einen booleschen Wert zurück. Aber warte, es ruft eine andere Methode auf, wie viele Spieler().

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

Dies ist im Grunde nur eine Methode, die die Elemente in der Klasse zählt. Spieler Array. OK, wenn wir also keine Spieler hinzufügen, sollte es Null sein. isPlayable () sollte falsch zurückgeben. Mal sehen, ob unsere Annahme richtig ist.

Funktion testAJustCreatedNewGameIsNotPlayable () $ game = new Game (); $ this-> assertFalse ($ game-> isPlayable ()); 

Wir haben unsere bisherige Testmethode umbenannt, um darzustellen, was wir wirklich testen möchten. Dann haben wir nur behauptet, das Spiel sei nicht spielbar. Der Test ist bestanden. In vielen Fällen sind jedoch falsch positive Ergebnisse verbreitet. Für den Verstand können wir also wahr behaupten und sicherstellen, dass der Test fehlschlägt.

$ this-> assertTrue ($ game-> isPlayable ());

Und das tut es!

PHPUnit_Framework_ExpectationFailedException: Die falsche Angabe von false ist wahr.

Bis jetzt ziemlich vielversprechend. Es ist uns gelungen, den ursprünglichen Rückgabewert der Methode zu testen, den Wert, der durch den ursprünglichen Wert dargestellt wird Zustand des Spiel Klasse. Bitte beachten Sie das hervorgehobene Wort: "state". Wir müssen einen Weg finden, um den Status des Spiels zu kontrollieren. Wir müssen es ändern, so dass es die minimale Anzahl von Spielern haben wird.

Wenn wir analysieren Spiel's hinzufügen() Methode werden wir sehen, dass es unserem Array Elemente hinzufügt.

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

Unsere Annahme wird durch die Art und Weise der hinzufügen() Methode wird in verwendet RunnerFunctions.php.

Funktion run () $ aGame = neues Spiel (); $ aGame-> add ("Chet"); $ aGame-> add ("Pat"); $ aGame-> add ("Sue"); //… //

Basierend auf diesen Beobachtungen können wir dies durch Verwendung von schließen hinzufügen() zweimal sollten wir unsere bringen können Spiel in einen Staat mit zwei Spielern.

Funktion testAfterAddingTwoPlayersToANewGameItIsPlayable () $ game = new Game (); $ game-> add ('Erster Spieler'); $ game-> add ('Second Player'); $ this-> assertTrue ($ game-> isPlayable ()); 

Durch das Hinzufügen dieser zweiten Testmethode können wir dies sicherstellen isPlayable () Gibt true zurück, wenn die Bedingungen erfüllt sind.

Aber Sie denken vielleicht, dass dies kein absoluter Unit-Test ist. Wir nehmen das hinzufügen() Methode! Wir üben mehr als das absolute Minimum an Code aus. Wir könnten stattdessen einfach die Elemente zum hinzufügen $ Spieler Array und nicht auf die verlassen hinzufügen() Methode überhaupt.

Nun, die Antwort lautet ja und nein. Das könnten wir aus technischer Sicht. Es hat den Vorteil einer direkten Kontrolle über das Array. Es hat jedoch den Nachteil, dass Code zwischen Tests und Code dupliziert wird. Wählen Sie also eine der schlechten Optionen, mit der Sie denken, dass Sie damit leben können, und verwenden Sie diese. Ich persönlich bevorzuge Methoden wie hinzufügen().

Umgestaltungstests

Wir sind auf Grün, wir gestalten um. Können wir unsere Tests besser machen? Nun ja, wir können. Wir könnten unseren ersten Test transformieren, um alle Bedingungen von nicht genügend Spielern zu überprüfen.

Funktion testAGameWithNotEnoughPlayersIsNotPlayable () $ game = new Game (); $ this-> assertFalse ($ game-> isPlayable ()); $ game-> add ('A player'); $ this-> assertFalse ($ game-> isPlayable ()); 

Sie haben vielleicht von dem Konzept "Eine Aussage pro Test" gehört. Ich stimme dem meistens zu, aber wenn Sie einen Test haben, der ein einzelnes Konzept überprüft und zur Bestätigung mehrere Assertions erfordert, ist es meiner Meinung nach akzeptabel, mehr als eine Assertion zu verwenden. Diese Ansicht wird auch von Robert C. Martin in seinen Lehren stark gefördert.

Aber wie sieht es mit unserer zweiten Testmethode aus? Ist das gut genug? Ich sage nein.

$ game-> add ('Erster Spieler'); $ game-> add ('Second Player');

Diese beiden Anrufe stören mich ein wenig. Sie sind eine detaillierte Implementierung ohne explizite Erklärung in unserer Methode. Warum extrahieren Sie sie nicht in eine private Methode?

Funktion testAfterAddingEnoughPlayersToANewGameItIsPlayable () $ game = new Game (); $ this-> addEnoughPlayers ($ game); $ this-> assertTrue ($ game-> isPlayable ());  private Funktion addEnoughPlayers ($ game) $ game-> add ('First Player'); $ game-> add ('Second Player'); 

Das ist viel besser und führt uns auch zu einem anderen Konzept, das wir vermisst haben. In beiden Tests haben wir auf die eine oder andere Weise das Konzept "genug Spieler" ausgedrückt. Aber wie viel ist genug? Sind es zwei Ja, jetzt ist es soweit. Aber wollen wir, dass unser Test fehlschlägt, wenn die Spiel's Logik erfordert mindestens drei Spieler? Wir möchten nicht, dass dies geschieht. Wir können dafür ein öffentliches statisches Klassenfeld einführen.

Klassenspiel static $ minimumNumberOfPlayers = 2; //… // Funktion __construct () //… // Funktion isPlayable () return ($ this-> howManyPlayers ()> = self :: $ minimumNumberOfPlayers);  //… //

Dies ermöglicht uns die Verwendung in unseren Tests.

private Funktion addEnoughPlayers ($ game) für ($ i = 0; $ i.) < Game::$minimumNumberOfPlayers; $i++)  $game->add ('A Player'); 

Unsere kleine Helfer-Methode fügt Spieler hinzu, bis genug hinzugefügt ist. Für unseren ersten Test können wir sogar noch eine solche Methode erstellen, also fügen wir fast genug Spieler hinzu.

Funktion testAGameWithNotEnoughPlayersIsNotPlayable () $ game = new Game (); $ this-> assertFalse ($ game-> isPlayable ()); $ this-> addJustNothEnoughPlayers ($ game); $ this-> assertFalse ($ game-> isPlayable ());  private Funktion addJustNothEnoughPlayers ($ game) für ($ i = 0; $ i.) < Game::$minimumNumberOfPlayers - 1; $i++)  $game->add ('Ein Spieler'); 

Dies führte jedoch zu einer Verdoppelung. Unsere beiden Hilfsmethoden sind ziemlich ähnlich. Können wir ihnen nicht ein drittes entnehmen??

private Funktion addEnoughPlayers ($ game) $ this-> addManyPlayers ($ game, Game :: $ minimumNumberOfPlayers);  private Funktion addJustNothEnoughPlayers ($ game) $ this-> addManyPlayers ($ game, Game :: $ minimumNumberOfPlayers - 1);  private Funktion addManyPlayers ($ game, $ numberOfPlayers) für ($ i = 0; $ i.) < $numberOfPlayers; $i++)  $game->add ('A Player'); 

Das ist besser, aber es führt zu einem anderen Problem. Wir haben die Duplizierung dieser Methoden reduziert, aber unsere $ game Das Objekt wird nun in drei Stufen weitergegeben. Es wird schwierig zu handhaben. Es ist Zeit, es im Test zu initialisieren Konfiguration() Methode und wiederverwenden.

class GameTest erweitert PHPUnit_Framework_TestCase privates $ game; function setUp () $ this-> game = neues Spiel;  Funktion testAGameWithNotEnoughPlayersIsNotPlayable () $ this-> assertFalse ($ this-> game-> isPlayable ()); $ this-> addJustNothEnoughPlayers (); $ this-> assertFalse ($ this-> game-> isPlayable ());  Funktion testAfterAddingEnoughPlayersToANewGameItIsPlayable () $ this-> addEnoughPlayers ($ this-> game); $ this-> assertTrue ($ this-> game-> isPlayable ());  private Funktion addEnoughPlayers () $ this-> addManyPlayers (Game :: $ minimumNumberOfPlayers);  private Funktion addJustNothEnoughPlayers () $ this-> addManyPlayers (Game :: $ minimumNumberOfPlayers - 1);  private Funktion addManyPlayers ($ numberOfPlayers) für ($ i = 0; $ i.) < $numberOfPlayers; $i++)  $this->game-> add ('A Player'); 

Viel besser. Der gesamte irrelevante Code ist in privaten Methoden, $ game wird in initialisiert Konfiguration() und eine Menge Verschmutzung wurde von den Testmethoden entfernt. Hier mussten wir jedoch einen Kompromiss eingehen. In unserem ersten Test beginnen wir mit einer Behauptung. Das setzt das voraus Konfiguration() wird immer ein leeres Spiel erstellen. Das ist jetzt in Ordnung. Aber am Ende des Tages müssen Sie feststellen, dass es keinen perfekten Code gibt. Es gibt nur Code mit Kompromissen, mit denen Sie leben möchten.

Die zweite überprüfbare Methode

Wenn wir unsere scannen Spiel Klasse von oben nach unten, ist die nächste Methode auf unserer Liste hinzufügen(). Ja, dieselbe Methode, die wir in unseren Tests im vorherigen Abschnitt verwendet haben. Aber können wir es testen??

Funktion testItCanAddANewPlayer () $ this-> game-> add ('A player'); $ this-> assertEquals (1, Anzahl ($ this-> game-> players)); 

Dies ist nun eine andere Art, Objekte zu testen. Wir rufen unsere Methode auf und überprüfen den Zustand des Objekts. Wie hinzufügen() kehrt immer zurück wahr, Es gibt keine Möglichkeit, die Ausgabe zu testen. Aber wir können mit einem leeren beginnen Spiel Objekt und prüfen Sie, ob es einen einzelnen Benutzer gibt, nachdem wir einen hinzugefügt haben. Aber ist das genug Bestätigung??

Funktion testItCanAddANewPlayer () $ this-> assertEquals (0, Anzahl ($ this-> game-> players)); $ this-> game-> add ('A player'); $ this-> assertEquals (1, Anzahl ($ this-> game-> players)); 

Wäre es nicht besser zu prüfen, ob vor dem Anruf keine Spieler anwesend sind? hinzufügen()? Nun, es kann ein bisschen zu viel sein, aber wie Sie im obigen Code sehen können, könnten wir es tun. Und wann immer Sie sich nicht sicher sind, sollten Sie eine Aussage darüber machen. Dies schützt Sie auch vor zukünftigen Codeänderungen, die den Anfangsstatus Ihres Objekts ändern können.

Aber testen wir alle Dinge die hinzufügen() Methode macht? Ich sage nein. Neben dem Hinzufügen eines Benutzers werden auch viele Einstellungen dafür festgelegt. Wir sollten auch nach denen suchen.

Funktion testItCanAddANewPlayer () $ this-> assertEquals (0, Anzahl ($ this-> game-> players)); $ this-> game-> add ('A player'); $ this-> assertEquals (1, Anzahl ($ this-> game-> players)); $ this-> assertEquals (0, $ this-> game-> places [1]); $ this-> assertEquals (0, $ this-> game-> geldbörsen [1]); $ this-> assertFalse ($ this-> game-> inPenaltyBox [1]); 

Das ist besser. Wir überprüfen jede Aktion, die die hinzufügen() Methode tut. Dieses Mal wollte ich das direkt testen $ Spieler Array. Warum? Wir hätten das benutzen können wie viele Spieler() Methode, die im Grunde das Gleiche tut, richtig? Nun, in diesem Fall haben wir es für wichtiger erachtet, unsere Behauptungen durch die Auswirkungen zu beschreiben, die die hinzufügen() Methode hat den Status des Objekts. Wenn wir uns ändern müssen hinzufügen(), Wir würden erwarten, dass der Test, der sein striktes Verhalten testet, fehlschlagen wird. Ich hatte endlose Diskussionen mit meinen Kollegen bei Syneto darüber. Vor allem deshalb, weil diese Art von Tests eine starke Kopplung zwischen dem Test und dem Testverfahren darstellt hinzufügen() Methode ist tatsächlich implementiert. Wenn Sie es also lieber andersherum testen möchten, heißt das nicht, dass Ihre Ideen falsch sind.

Wir können das Testen der Ausgabe mit Sicherheit ignorieren Echoln () Zeilen. Sie geben lediglich Inhalte auf dem Bildschirm aus. Wir wollen diese Methoden noch nicht anfassen. Unser goldener Meister verlässt sich vollständig auf diesen Output.

Umgestaltungstests (Bis)

Wir haben eine weitere getestete Methode mit einem brandneuen Passtest. Es ist Zeit, beide umzuwandeln, nur ein bisschen. Beginnen wir mit unseren Tests. Sind die letzten drei Behauptungen nicht etwas verwirrend? Sie scheinen nicht in direktem Zusammenhang mit dem Hinzufügen eines Spielers zu stehen. Lass es uns ändern:

Funktion testItCanAddANewPlayer () $ this-> assertEquals (0, Anzahl ($ this-> game-> players)); $ this-> game-> add ('A player'); $ this-> assertEquals (1, Anzahl ($ this-> game-> players)); $ this-> assertDefaultPlayerParametersAreSetFor (1); 

Das ist besser. Die Methode ist jetzt abstrakter, wiederverwendbar, ausdrucksvoll benannt und verbirgt alle unwichtigen Details.

Umgestaltung der hinzufügen() Methode

Mit unserem Produktionscode können wir etwas Ähnliches machen.

Funktion add ($ playerName) array_push ($ this-> players, $ playerName); $ this-> setDefaultPlayerParametersFor ($ this-> howManyPlayers ()); echoln ($ playerName. "wurde hinzugefügt"); echoln ("Sie sind Spielernummer". Anzahl ($ this-> Spieler)); wahr zurückgeben; 

Wir haben die unwichtigen Details herausgezogen setDefaultPlayerParametersFor ().

private Funktion setDefaultPlayerParametersFor ($ playerId) $ this-> places [$ playerId] = 0; $ this-> Geldbörsen [$ playerId] = 0; $ this-> inPenaltyBox [$ playerId] = false; 

Eigentlich kam mir diese Idee, nachdem ich den Test geschrieben hatte. Dies ist ein weiteres schönes Beispiel dafür, wie Tests uns zwingen, unseren Code aus einer anderen Perspektive zu betrachten. Diesen unterschiedlichen Blickwinkel auf das Problem müssen wir ausnutzen und unsere Tests die Gestaltung des Produktionscodes bestimmen lassen.

Die dritte überprüfbare Methode

Suchen wir unseren dritten Kandidaten zum Testen. wie viele Spieler() ist zu einfach und indirekt bereits getestet. rollen() ist zu komplex, um direkt getestet zu werden. Plus es kehrt zurück Null. Fragen stellen() scheint auf den ersten Blick interessant zu sein, aber es ist alles Präsentation, kein Rückgabewert.

currentCategory () ist testbar, aber es ist hübsch schwer zu testen. Es ist eine riesige Auswahl mit zehn Bedingungen. Wir brauchen einen zehnzeiligen Test, und dann müssen wir diese Methode und sicherlich auch die Tests ernsthaft überarbeiten. Wir sollten diese Methode zur Kenntnis nehmen und darauf zurückkommen, nachdem wir mit den einfacheren fertig sind. Für uns wird dies in unserem nächsten Tutorial sein.

wasCorrectlyAnswered () ist wieder zu kompliziert. Wir müssen daraus kleine Codeteile extrahieren, die überprüfbar sind. jedoch, falsche Antwort() scheint vielversprechend. Es gibt Sachen auf dem Bildschirm aus, aber es ändert auch den Zustand unseres Objekts. Mal sehen, ob wir es kontrollieren und testen können.

Funktion testWhenAPlayerEntersAWrongAnswerItIsSentToThePenaltyBox () $ this-> game-> add ('A player'); $ this-> game-> currentPlayer = 0; $ this-> game-> wrongAnswer (); $ this-> assertTrue ($ this-> game-> inPenaltyBox [0]); 

Grrr… Es war ziemlich schwierig, diese Testmethode zu schreiben. falsche Antwort() beruht auf $ this-> currentPlayer für seine Verhaltenslogik, aber es verwendet auch $ this-> Spieler in seinem Präsentationsteil. Ein hässliches Beispiel, warum Sie Logik und Präsentation nicht mischen sollten. Wir werden uns in einem zukünftigen Tutorial damit befassen. Wir haben vorerst getestet, dass der Benutzer die Strafbox eingibt. Wir müssen auch feststellen, dass es eine gibt ob() Anweisung in der Methode. Dies ist eine Bedingung, die wir noch nicht testen, da wir nur einen einzelnen Spieler haben und daher die Bedingung nicht erfüllen. Wir könnten den Endwert von testen $ currentPlayer obwohl. Wenn Sie diese Codezeile jedoch zum Test hinzufügen, schlägt der Test fehl.

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

Ein genauerer Blick auf die private Methode shouldResetCurrentPlayer () enthüllt das Problem. Wenn der Index des aktuellen Spielers der Anzahl der Spieler entspricht, wird er auf Null zurückgesetzt. Aaaahhh! Wir geben tatsächlich die ob()!

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

Gut. Wir haben einen zweiten Test erstellt, um den konkreten Fall zu testen, wenn noch Spieler spielen, die nicht gespielt haben. Das interessiert uns nicht inPenaltyBox Zustand für den zweiten Test. Uns interessiert nur der Index des aktuellen Players.

Die letzte überprüfbare Methode

Die letzte Methode, die wir testen und dann umgestalten können, ist didPlayerWin ().

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

Wir können sofort feststellen, dass seine Codestruktur sehr ähnlich ist isPlayable (), die Methode, die wir zuerst getestet haben. Unsere Lösung sollte auch etwas ähnliches sein. Wenn Ihr Code so kurz ist, nur zwei bis drei Zeilen, ist es nicht so riskant, mehr als einen winzigen Schritt zu machen. Im schlimmsten Fall setzen Sie drei Codezeilen zurück. Also machen wir das in einem einzigen Schritt.

Funktion testTestPlayerWinsWithTheCorrectNumberOfCoins () $ this-> game-> currentPlayer = 0; $ this-> game-> geldbörsen [0] = Spiel :: $ numberOfCoinsToWin; $ this-> assertTrue ($ this-> game-> didPlayerWin ()); 

Aber warte! Das scheitert Wie ist das möglich? Sollte es nicht passieren? Wir haben die korrekte Anzahl von Münzen angegeben. Wenn wir unsere Methode studieren, werden wir eine etwas irreführende Tatsache entdecken.

return! ($ this-> geldbörsen [$ this-> currentPlayer] == $ numberOfCoinsToWin);

Der Rückgabewert wird tatsächlich negiert. Die Methode sagt uns also nicht, ob ein Spieler gewonnen hat, sondern, ob ein Spieler das Spiel nicht gewonnen hat. Wir könnten hineingehen und die Orte finden, an denen diese Methode verwendet wird, und ihren Wert dort negieren. Dann ändern Sie ihr Verhalten hier, um die Antwort nicht falsch zu negieren. Aber es wird in verwendet wasCorrectlyAnswered (), Eine Methode, die wir noch nicht testen können. Möglicherweise reicht vorerst ein einfaches Umbenennen, um die korrekte Funktionalität hervorzuheben.

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

Gedanken & Schlussfolgerung

Damit rundet das Tutorial ab. Wir mögen die Negation im Namen nicht, aber dies ist ein Kompromiss, den wir an dieser Stelle treffen können. Dieser Name wird sich sicherlich ändern, wenn wir andere Teile des Codes umgestalten. Wenn Sie sich unsere Tests anschauen, sehen sie außerdem merkwürdig aus:

Funktion testTestPlayerWinsWithTheCorrectNumberOfCoins () $ this-> game-> currentPlayer = 0; $ this-> game-> geldbörsen [0] = Spiel :: $ numberOfCoinsToWin; $ this-> assertFalse ($ this-> game-> didPlayerNotWin ()); 

Durch das Testen von false auf einer negierten Methode, die mit einem Wert ausgeübt wird, der ein wahres Ergebnis vermuten lässt, haben wir die Lesbarkeit unserer Codes sehr verwirrt. Aber das ist jetzt gut, da wir irgendwann mal aufhören müssen, richtig?

In unserem nächsten Tutorial werden wir an einigen der schwierigeren Methoden innerhalb des arbeiten Spiel Klasse. Danke fürs Lesen.