Refactoring von Legacy-Code: Teil 5 – Eine Möglichkeit, Ihr Spiel testbar zu machen

WBOY
Freigeben: 2023-08-28 16:30:01
Original
927 Leute haben es durchsucht

重构遗留代码:第五部分 - 游戏可测试的方法

Alter Code. Hässlicher Code. Komplexer Code. Spaghetti-Code. Unsinn. Kurz gesagt, Legacy-Code. Dies ist eine Serie, die Ihnen bei der Arbeit und beim Umgang mit Ihren Problemen helfen soll.

Im vorherigen Tutorial haben wir die Runner-Funktion getestet. In dieser Lektion ist es an der Zeit, dort weiterzumachen, wo wir beim Testen der Game-Klasse aufgehört haben. Wenn Sie nun mit einem großen Codeblock beginnen, wie wir ihn hier haben, ist es einfach, Methode für Methode von oben nach unten zu testen. Meistens ist dies nicht möglich. Beginnen Sie den Test am besten mit einem kurzen, testbaren Ansatz. Das ist es, was wir in dieser Lektion tun werden: diese Methoden finden und testen.

Erstelle ein Spiel

Um eine Klasse zu testen, müssen wir ein Objekt dieses bestimmten Typs initialisieren. Wir können uns unseren ersten Test als die Erstellung eines solchen neuen Objekts vorstellen. Sie werden überrascht sein, wie viele Geheimnisse ein Konstrukteur verbergen kann.

require_once __DIR__ . '/../trivia/php/Game.php';

class GameTest extends PHPUnit_Framework_TestCase {

	function testWeCanCreateAGame() {
		$game = new Game();
	}

}
Nach dem Login kopieren

Zu unserer Überraschung ist Game 实际上可以很容易地创建。仅运行 new Game() 时没有问题。没有任何损坏。这是一个非常好的开始,特别是考虑到 Game eigentlich ganz einfach zu erstellen. Es gibt kein Problem, wenn Sie einfach new Game() ausführen. Es liegt kein Schaden vor. Das ist ein wirklich guter Anfang, vor allem wenn man bedenkt, dass der Konstruktor von

recht groß ist und viele Aufgaben erledigt.

Auf der Suche nach der ersten testbaren Methode

Jetzt möchte ich den Konstruktor wirklich vereinfachen. Aber wir haben nur finanzielle Unterstützer, um sicherzustellen, dass wir nichts kaputt machen. Bevor wir mit dem Konstruktor beginnen, müssen wir den Großteil der restlichen Klasse testen. Wo sollen wir also 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 es ein guter Kandidat für unsere Tests.

function isPlayable() {
	$minimumNumberOfPlayers = 2;
	return ($this->howManyPlayers() >= $minimumNumberOfPlayers);
}
Nach dem Login kopieren
howManyPlayers()Wie wäre es mit dieser Methode? Das scheint ein guter Kandidat zu sein. Es gibt nur zwei Zeilen und es wird ein boolescher Wert zurückgegeben. Aber warten Sie, es ruft eine andere Methode auf,

.

function howManyPlayers() {
	return count($this->players);
}
Nach dem Login kopieren
players 数组中元素的方法。好的,所以如果我们不添加任何玩家,它应该为零。 isPlayable()Dies ist im Grunde nur eine Berechnungsklasse

, die false zurückgeben sollte. Mal sehen, ob unsere Annahmen richtig sind.

function testAJustCreatedNewGameIsNotPlayable() {
	$game = new Game();
	$this->assertFalse($game->isPlayable());
}
Nach dem Login kopieren

Wir haben die vorherige Testmethode umbenannt, um widerzuspiegeln, was wir eigentlich testen wollten. Wir haben dann behauptet, dass das Spiel unspielbar sei. Test bestanden. In vielen Fällen kommt es jedoch häufig zu Fehlalarmen. Aus Sicherheitsgründen können wir also die Aussage wahr machen und sicherstellen, dass der Test fehlschlägt.

$this->assertTrue($game->isPlayable());
Nach dem Login kopieren

In der Tat!

PHPUnit_Framework_ExpectationFailedException :
Failed asserting that false is true.
Nach dem Login kopieren
GameBisher sehr vielversprechend. Es ist uns gelungen, den anfänglichen Rückgabewert der Methode zu testen, der durch den anfänglichen Zustand der

-Klasse dargestellt wird. Beachten Sie das hervorgehobene Wort: „Staat“. Wir müssen einen Weg finden, den Zustand des Spiels zu kontrollieren. Wir müssen es so ändern, dass es die Mindestanzahl an Spielern hat.

Gameadd()Wenn wir die

-Methode von

analysieren, sehen wir, dass sie Elemente zu unserem Array hinzufügt. RunnerFunctions.php 中使用 add()

array_push($this->players, $playerName);
Nach dem Login kopieren

Unsere Annahmen werden in der

-Methode durchgesetzt. add() 两次,我们应该能够使 Game

function run() {

	$aGame = new Game();
	$aGame->add("Chet");
	$aGame->add("Pat");
	$aGame->add("Sue");

	// ... //
}
Nach dem Login kopieren

Basierend auf diesen Beobachtungen können wir schlussfolgern, dass wir durch die doppelte Verwendung von

in der Lage sein sollten, isPlayable() in einen Zustand mit zwei Spielern zu bringen.

function testAfterAddingTwoPlayersToANewGameItIsPlayable() {
	$game = new Game();
	$game->add('First Player');
	$game->add('Second Player');
	$this->assertTrue($game->isPlayable());
}
Nach dem Login kopieren

Durch das Hinzufügen einer zweiten Testmethode können wir sicherstellen, dass add() 方法!我们练习的不仅仅是最少的代码。我们可以只将元素添加到 $players 数组中,而根本不依赖 add() „true“ zurückgibt, wenn die Bedingung erfüllt ist.

Aber Sie denken vielleicht, dass dies nicht gerade ein Unit-Test ist. Wir verwenden die add()-Methode.

Nun, die Antwort ist ja und nein. Aus technischer Sicht können wir dies tun. Dies hätte den Vorteil einer direkten Steuerung des Arrays. Allerdings besteht der Nachteil der Codeduplizierung zwischen Code und Tests. Wählen Sie also eine der beschissenen Optionen aus, mit denen Sie Ihrer Meinung nach leben können, und nutzen Sie sie. Ich persönlich bevorzuge den Wiederverwendungsansatz.

Refactoring-Tests

Wir befinden uns im grünen Status und führen eine Umgestaltung durch. Können wir unsere Tests verbessern? Ja, das können wir. Wir können unseren ersten Test ändern, um alle Bedingungen ohne genügend Spieler zu überprüfen.

function testAGameWithNotEnoughPlayersIsNotPlayable() {
	$game = new Game();
	$this->assertFalse($game->isPlayable());
	$game->add('A player');
	$this->assertFalse($game->isPlayable());
}
Nach dem Login kopieren

Vielleicht haben Sie schon einmal von dem Konzept „eine Behauptung pro Test“ gehört. Dem stimme ich grundsätzlich zu, aber wenn Sie einen Test haben, der ein einzelnes Konzept validiert und für die Validierung mehrere Assertionen erfordert, halte ich es für akzeptabel, mehrere Assertionen zu verwenden. Diese Ansicht wird auch in den Lehren von Robert C. Martin stark vertreten.

Aber was ist mit unserer zweiten Testmethode? Reicht das? Ich sagte nein.

$game->add('First Player');
$game->add('Second Player');
Nach dem Login kopieren
🎜Diese beiden Anrufe stören mich ein wenig. Es handelt sich um detaillierte Implementierungen, die in unserem Ansatz nicht explizit erläutert werden. Warum extrahieren Sie sie nicht in private Methoden? 🎜
function testAfterAddingEnoughPlayersToANewGameItIsPlayable() {
	$game = new Game();
	$this->addEnoughPlayers($game);
	$this->assertTrue($game->isPlayable());
}

private function addEnoughPlayers($game) {
	$game->add('First Player');
	$game->add('Second Player');
}
Nach dem Login kopieren

这要好得多,它也让我们想到了另一个我们错过的概念。在这两次测试中,我们都以这样或那样的方式表达了“足够多的玩家”的概念。但多少才够呢?是两个吗?是的,目前是这样。但是,如果 Game 的逻辑需要至少三个玩家,我们是否希望测试失败?我们不希望这种情况发生。我们可以为其引入一个公共静态类字段。

class Game {
	static $minimumNumberOfPlayers = 2;

	// ... //

	function  __construct() {
		// ... //
	}

	function isPlayable() {
		return ($this->howManyPlayers() >= self::$minimumNumberOfPlayers);
	}

	// ... //
}
Nach dem Login kopieren

这将使我们能够在测试中使用它。

private function addEnoughPlayers($game) {
	for($i = 0; $i < Game::$minimumNumberOfPlayers; $i++) {
		$game->add('A Player');
	}
}
Nach dem Login kopieren

我们的小助手方法只会添加玩家,直到添加足够的玩家为止。我们甚至可以为我们的第一次测试创建另一个这样的方法,因此我们添加了几乎足够的玩家。

function testAGameWithNotEnoughPlayersIsNotPlayable() {
	$game = new Game();
	$this->assertFalse($game->isPlayable());
	$this->addJustNothEnoughPlayers($game);
	$this->assertFalse($game->isPlayable());
}

private function addJustNothEnoughPlayers($game) {
	for($i = 0; $i < Game::$minimumNumberOfPlayers - 1; $i++) {
		$game->add('A player');
	}
}
Nach dem Login kopieren

但这引入了一些重复。我们的两个辅助方法非常相似。我们不能从中提取第三个吗?

private function addEnoughPlayers($game) {
	$this->addManyPlayers($game, Game::$minimumNumberOfPlayers);
}

private function addJustNothEnoughPlayers($game) {
	$this->addManyPlayers($game, Game::$minimumNumberOfPlayers - 1);
}

private function addManyPlayers($game, $numberOfPlayers) {
	for ($i = 0; $i < $numberOfPlayers; $i++) {
		$game->add('A Player');
	}
}
Nach dem Login kopieren

这更好,但它引入了一个不同的问题。我们减少了这些方法中的重复,但是我们的 $game 对象现在向下传递了三个级别。管理变得越来越困难。是时候在测试的 setUp() 方法中初始化它并重用它了。

class GameTest extends PHPUnit_Framework_TestCase {

	private $game;

	function setUp() {
		$this->game = new Game;
	}

	function testAGameWithNotEnoughPlayersIsNotPlayable() {
		$this->assertFalse($this->game->isPlayable());
		$this->addJustNothEnoughPlayers();
		$this->assertFalse($this->game->isPlayable());
	}

	function testAfterAddingEnoughPlayersToANewGameItIsPlayable() {
		$this->addEnoughPlayers($this->game);
		$this->assertTrue($this->game->isPlayable());
	}

	private function addEnoughPlayers() {
		$this->addManyPlayers(Game::$minimumNumberOfPlayers);
	}

	private function addJustNothEnoughPlayers() {
		$this->addManyPlayers(Game::$minimumNumberOfPlayers - 1);
	}

	private function addManyPlayers($numberOfPlayers) {
		for ($i = 0; $i < $numberOfPlayers; $i++) {
			$this->game->add('A Player');
		}
	}

}
Nach dem Login kopieren

好多了。所有不相关的代码都在私有方法中,$gamesetUp()中初始化,并且从测试方法中去除了很多污染。然而,我们确实必须在这里做出妥协。在我们的第一个测试中,我们从一个断言开始。这假设 setUp() 将始终创建一个空游戏。现在这样就可以了。但最终,您必须意识到不存在完美的代码。只有您愿意接受的妥协代码。

第二种可测试方法

如果我们从上到下扫描 Game 类,列表中的下一个方法是 add()。是的,与我们在上一段测试中使用的方法相同。但我们可以测试一下吗?

function testItCanAddANewPlayer() {
	$this->game->add('A player');
	$this->assertEquals(1, count($this->game->players));
}
Nach dem Login kopieren

现在这是测试对象的不同方式。我们调用我们的方法,然后验证对象的状态。由于 add() 总是返回 true,因此我们无法测试其输出。但是我们可以从一个空的 Game 对象开始,然后在添加一个用户后检查是否有单个用户。但这足够验证吗?

function testItCanAddANewPlayer() {
	$this->assertEquals(0, count($this->game->players));
	$this->game->add('A player');
	$this->assertEquals(1, count($this->game->players));
}
Nach dem Login kopieren

在调用 add() 之前先验证一下是否没有玩家不是更好吗?好吧,这里可能有点太多了,但正如您在上面的代码中看到的,我们可以做到。当你不确定初始状态时,你应该对其进行断言。这还可以保护您免受将来可能会更改对象初始状态的代码更改的影响。

但是我们是否测试了 add() 方法所做的所有事情?我拒绝。除了添加用户之外,它还为其设置了很多设置。我们还应该检查这些。

function testItCanAddANewPlayer() {
	$this->assertEquals(0, count($this->game->players));
	$this->game->add('A player');
	$this->assertEquals(1, count($this->game->players));
	$this->assertEquals(0, $this->game->places[1]);
	$this->assertEquals(0, $this->game->purses[1]);
	$this->assertFalse($this->game->inPenaltyBox[1]);
}
Nach dem Login kopieren

这样更好。我们验证 add() 方法执行的每个操作。这次,我更愿意直接测试 $players 数组。为什么?我们可以使用 howManyPlayers() 方法,它基本上做同样的事情,对吗?好吧,在这种情况下,我们认为更重要的是通过 add() 方法对对象状态的影响来描述我们的断言。如果我们需要更改 add(),我们预计测试其严格行为的测试将会失败。我和 Syneto 的同事就这个问题进行了无休止的争论。特别是因为这种类型的测试在测试与 add() 方法的实际实现方式之间引入了强耦合。因此,如果您更愿意以相反的方式进行测试,这并不意味着您的想法是错误的。

我们可以安全地忽略对输出的测试,即 echoln() 行。他们只是在屏幕上输出内容。我们还不想触及这些方法。我们的金主完全靠这个输出。

重构测试(之二)

我们有另一种经过测试的方法,通过了全新的测试。是时候重构它们了,只是一点点。让我们从测试开始。最后三个断言是不是有点令人困惑?它们似乎与添加玩家没有严格关系。让我们改变它:

function testItCanAddANewPlayer() {
	$this->assertEquals(0, count($this->game->players));
	$this->game->add('A player');
	$this->assertEquals(1, count($this->game->players));
	$this->assertDefaultPlayerParametersAreSetFor(1);
}
Nach dem Login kopieren

这样更好。该方法现在更加抽象、可重用、命名更明确,并且隐藏了所有不重要的细节。

重构 add() 方法

我们可以用我们的生产代码做类似的事情。

function add($playerName) {
	array_push($this->players, $playerName);
	$this->setDefaultPlayerParametersFor($this->howManyPlayers());

	echoln($playerName . " was added");
	echoln("They are player number " . count($this->players));
	return true;
}
Nach dem Login kopieren

我们将不重要的细节提取到setDefaultPlayerParametersFor()中。

private function setDefaultPlayerParametersFor($playerId) {
	$this->places[$playerId] = 0;
	$this->purses[$playerId] = 0;
	$this->inPenaltyBox[$playerId] = false;
}
Nach dem Login kopieren

其实这个想法是我写完测试之后就产生的。这是另一个很好的例子,说明测试如何迫使我们从不同的角度思考我们的代码。我们必须利用这种看待问题的不同角度,并让我们的测试指导我们的生产代码设计。

第三种可测试方法

让我们找到第三个候选者进行测试。 howManyPlayers() 太简单并且已经间接测试过。 roll() 太复杂,无法直接测试。另外它返回 nullaskQuestions() 乍一看似乎很有趣,但它都是演示,没有返回值。

currentCategory() 是可测试的,但测试起来非常困难。这是一个巨大的选择器,有十个条件。我们需要一个十行长的测试,然后我们需要认真地重构这个方法,当然还有测试。我们应该记下这个方法,并在完成更简单的方法后再回来使用它。对于我们来说,这将出现在我们的下一个教程中。

wasCorrectlyAnswered() 又变得复杂了。我们需要从中提取可测试的小段代码。然而, wrongAnswer() 似乎很有前途。它在屏幕上输出内容,但它也会改变对象的状态。让我们看看是否可以控制它并测试它。

function testWhenAPlayerEntersAWrongAnswerItIsSentToThePenaltyBox() {
	$this->game->add('A player');
	$this->game->currentPlayer = 0;
	$this->game->wrongAnswer();
	$this->assertTrue($this->game->inPenaltyBox[0]);
}
Nach dem Login kopieren

Grrr...编写这个测试方法相当困难。 wrongAnswer() 的行为逻辑依赖于 $this->currentPlayer,但在其表示部分也使用了 $this->players。一个丑陋的例子说明了为什么你不应该混合逻辑和表示。我们将在以后的教程中处理这个问题。现在,我们测试了用户进入惩罚框。我们还必须观察到该方法中有一个 if() 语句。这是我们尚未测试的条件,因为我们只有一个玩家,因此我们不满足该条件。不过,我们可以测试 $currentPlayer 的最终值。但是将这行代码添加到测试中将会导致测试失败。

$this->assertEquals(1, $this->game->currentPlayer);
Nach dem Login kopieren

仔细看看私有方法 shouldResetCurrentPlayer() 就会发现问题。如果当前玩家的索引等于玩家数量,则将其重置为零。啊啊啊!我们实际上输入的是if()!

function 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);
}
Nach dem Login kopieren

好。我们创建了第二个测试,以测试仍然有玩家没有玩的情况。我们不关心第二次测试的 inPenaltyBox 状态。我们只对当前玩家的索引感兴趣。

最终的可测试方法

我们可以测试然后重构的最后一个方法是 didPlayerWin()

function didPlayerWin() {
	$numberOfCoinsToWin = 6;
	return !($this->purses[$this->currentPlayer] == $numberOfCoinsToWin);
}
Nach dem Login kopieren

我们立即可以观察到它的代码结构与我们首先测试的方法 isPlayable() 非常相似。我们的解决方案也应该类似。当你的代码如此短,只有两到三行时,执行不止一个小步骤并不是那么大的风险。在最坏的情况下,您将恢复三行代码。因此,让我们一步完成此操作。

function testTestPlayerWinsWithTheCorrectNumberOfCoins() {
	$this->game->currentPlayer = 0;
	$this->game->purses[0] = Game::$numberOfCoinsToWin;
	$this->assertTrue($this->game->didPlayerWin());
}
Nach dem Login kopieren

但是等等!那失败了。这怎么可能?不应该过去吗?我们提供了正确数量的硬币。如果我们研究我们的方法,我们会发现一些误导性的事实。

return !($this->purses[$this->currentPlayer] == $numberOfCoinsToWin);
Nach dem Login kopieren

返回值实际上是取反的。因此,该方法并不是告诉我们玩家是否获胜,而是告诉我们玩家是否没有赢得比赛。我们可以进去找到使用该方法的地方,并在那里否定它的价值。然后在此处更改其行为,以免错误地否定答案。但它是在 wasCorrectlyAnswered() 中使用的,我们还无法对这个方法进行单元测试。也许暂时,简单的重命名以突出显示正确的功能就足够了。

function didPlayerNotWin() {
	return !($this->purses[$this->currentPlayer] == self::$numberOfCoinsToWin);
}
Nach dem Login kopieren

想法与结论

教程到此就结束了。虽然我们不喜欢名称中的否定,但这是我们目前可以做出的妥协。当我们开始重构代码的其他部分时,这个名称肯定会改变。此外,如果您看一下我们的测试,它们现在看起来很奇怪:

function testTestPlayerWinsWithTheCorrectNumberOfCoins() {
	$this->game->currentPlayer = 0;
	$this->game->purses[0] = Game::$numberOfCoinsToWin;
	$this->assertFalse($this->game->didPlayerNotWin());
}
Nach dem Login kopieren

通过在否定方法上测试 false,并使用表明真实结果的值来执行,我们给代码的可读性带来了很多混乱。但这目前来说很好,因为我们确实需要在某个时候停下来,对吗?

在下一个教程中,我们将开始研究 Game 类中的一些更困难的方法。感谢您的阅读。

Das obige ist der detaillierte Inhalt vonRefactoring von Legacy-Code: Teil 5 – Eine Möglichkeit, Ihr Spiel testbar zu machen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Quelle:php.cn
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage
Über uns Haftungsausschluss Sitemap
Chinesische PHP-Website:Online-PHP-Schulung für das Gemeinwohl,Helfen Sie PHP-Lernenden, sich schnell weiterzuentwickeln!