实施红-绿-重构模式
红-绿-重构模式 是一种实施 TDD 的编程方法。在这个循环中,你首先要故意编写一个失败的测试,在执行测试时,你会看到一个红色的失败信息。然后,编写解决方案代码以通过测试,这时你会看到一条绿色的通过信息。测试通过后,您可以回头清理并重构测试和解决方案代码。
如果打开我们在本书 第 5 章单元测试 中创建的 codebase/symfony/runDebug.sh
文件,你会发现我们在运行 PHPUnit 时添加了 --color=always
参数。然后,每当我们运行 PHPUnit 并导致测试失败时,你会发现我们总是会收到一条红色的错误或测试失败信息。
为了清楚地演示该模式,让我们举例说明:
-
创建一个名为
HelloTest.php
的新文件:codebase/symfony/tests/Unit/HelloTest.php<?php namespace App\Tests\Unit; use PHPUnit\Framework\TestCase; class HelloTest extends TestCase { public function testCanSayHello() { $this->fail("--- RED ---"); } }
-
创建新的单元测试后,运行以下命令以确保 PHPUnit 可以执行
testCanSayHello
测试:/var/www/html/symfony# php bin/phpunit --filter testCanSayHello --color=always
然后您应该看到以下结果:

在 TDD 中,我们总是首先编写一个没有实现支持或通过的测试。然后我们需要运行测试以确保 PHPUnit 识别该测试并且可以执行它。我们还想确认我们已经在正确的测试套件和正确的目录中创建了测试类,并且它使用了正确的命名空间。
运行前面所述的命令后,这个新创建的测试将按预期失败,并且 PHPUnit 将显示红色错误或失败消息。这是红-绿-重构模式中的红色!
一旦我们确定可以使用 PHPUnit 来运行测试,我们就可以继续编写代码来通过失败的测试。还记得 TDD 吗?我们的测试将启动或驱动解决方案代码的创建来解决问题,因此是测试驱动的。因此,现在,为了快速通过失败的测试,我们将按照以下步骤编写一些代码来通过失败的测试:
-
修改测试并添加一个新类:
Codebase/symfony/tests/Unit/HelloTest.php<?php namespace App\Tests\Unit; use App\Speaker; use PHPUnit\Framework\TestCase; class HelloTest extends TestCase { public function testCanSayHello() { $speaker = new Speaker(); $this->assertEquals('Hello' $speaker->sayHello()); } }
-
创建一个新类:
codebase/symfony/src/Speaker.php<?php namespace App; class Speaker { public function sayHello(): string { return 'Hello'; } }
在
HelloTest
类中,我们修改了testCanSayHello()
方法,以便它将创建我们创建的新Speaker
类的实例,然后,在断言行中,我们直接将预期的单词Hello
与sayHello()
返回的字符串进行比较。现在,如果我们再次运行测试,我们应该不会再看到红色的失败消息。 -
使用以下命令运行相同的测试:
/var/www/html/symfony# php bin/phpunit --filter testCanSayHello --color=always
我们现在应该从 PHPUnit 中看到以下结果:

我们通过了测试!现在,我们的 testCanSayHello()
测试不再返回红色错误/失败消息。我们做了最少的工作来通过测试,现在我们可以看到一条绿色的 OK(1 个测试,1 个断言)消息。这是红-绿-重构模式中的绿色。
当您处理自己的项目时,在通过测试后的这个阶段,您可以继续处理下一个测试或要做的事情列表中的下一个问题,或者您可以尝试改进测试和解决方案代码使其更清晰、更易于阅读。
在此示例中,我们将继续改进测试和解决方案代码,使其支持更多测试场景。
请按照以下步骤操作:
-
修改
HelloTest
类,内容如下:codebase/symfony/tests/Unit/HelloTest.php<?php namespace App\Tests\Unit; use App\Speaker; use PHPUnit\Framework\TestCase; class HelloTest extends TestCase { /** * @param \Closure $func * @param string $expected * @dataProvider provideHelloStrings */ public function testCanSayHello(\Closure $func, string $expected) { $speaker = new Speaker(); $helloMessage = $speaker->sayHello($func); $this->assertEquals($expected, $helloMessage); } /** * @return array[] */ private function provideHelloStrings(): array { return [ [function($str) {return ucfirst($str);}, 'Hello'], [function($str) {return strtolower($str);}, 'hello'], [function($str) {return strtoupper($str);}, 'HELLO'], ]; } }
-
使用以下内容修改
Speaker.php
类:codebase/symfony/src/Speaker.php<?php namespace App; class Speaker { /** * @return string */ public function sayHello(\Closure $func): string { return $func('Hello'); } }
我们重构了测试,以便可以为
Speaker.php
类添加更多灵活性。我们还重构了HelloTest.php
测试类本身,使其更加灵活。如果我们再次运行测试,我们仍然应该通过测试。 -
通过运行以下命令再次运行测试:
/var/www/html/symfony# php bin/phpunit --filter testCanSayHello --color=always
现在,我们应该看到以下结果:

您会注意到,由于我们只执行了一个测试,所以得到的结果不是 OK(1 个测试,1 个断言),而是 OK(3 个测试,3 个断言)。这是因为我们重构了测试,使其可以使用 @dataProvider
。然后,我们创建了一个名为 provideHelloStrings()
的新函数,该函数返回一个包含闭包和字符串的数组。每个数组集都将用作 testCanSayHello()
测试方法的参数。在这一阶段,即使我们进行了重构,仍然可以通过测试。这就是 红-绿-重构模式 的重构阶段。
在现实世界的企业项目中,编写程序时依赖别人的项目是很常见的,而你或你的团队并不能随时获得别人的项目。这是否应该阻止你开发依赖于尚未完成项目的程序呢?也许不需要!接下来,我们需要一种方法来集中测试应用程序的特定部分,即使它依赖于其他尚未构建的对象。为此,我们需要使用 mock
对象。