使用里氏替换原则的 TDD
LSP 由 Barbara Liskov 提出。我使用它的方式是,一个接口的实现应该可以用该接口的另一个实现来替代,而不会改变行为。如果您要扩展一个超类,那么子类必须能够在不破坏行为的情况下替换超类。
在本例中,让我们尝试添加一条业务规则,拒绝接受 1950 年或之前生产的玩具汽车模型。
像往常一样,让我们从测试开始:
-
打开我们之前创建的
YearValidatorTest.php
类并使用以下内容修改测试类:codebase/symfony/tests/Unit/Validator/YearValidatorTest.php<?php namespace App\Tests\Unit\Validator; use App\Validator\ToyCarTooOldException; use PHPUnit\Framework\TestCase; use App\Validator\YearValidator; class YearValidatorTest extends TestCase { /** * @param $data * @param $expected * @dataProvider provideYear */ public function testCanValidateYear(int $year, bool $expected): void { $validator = new YearValidator(); $isValid = $validator->validate($year); $this->assertEquals($expected, $isValid); } /** * @return array */ public function provideYear(): array { return [ [1, false], [2005, true], [1955, true], [312, false], ]; } /** * @param int $year * @dataProvider provideOldYears */ public function testCanRejectVeryOldCar(int $year): void { $this->expectException(ToyCarTooOldException::class); $validator = new YearValidator(); $validator->validate($year); } /** * @return array */ public function provideOldYears(): array { return [ [1944], [1933], [1922], [1911], ]; } }
-
我们添加了一个新测试,以便检查
ToyCarTooOldException
。我们也添加这个异常类,但首先,让我们运行测试。 -
运行以下命令:
/var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter testCanRejectVeryOldCar
-
现在您将看到四个错误。没关系。现在,让我们添加缺少的异常类:
codebase/symfony/src/Validator/ToyCarTooOldException.php<?php namespace App\Validator; class ToyCarTooOldException extends \Exception { }
如你所见,这只是一个简单的异常类,它扩展了主要的 PHP
\Exception
类。如果我们再次运行测试,现在应该可以通过测试了,因为我们已经通过使用
$this→expectException()
方法告诉 PHPUnit,我们期待在此测试中出现异常。 -
运行以下命令:
/var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter testCanRejectVeryOldCar
现在,我们应该能够通过测试 - 您应该看到以下结果:

这意味着每当我们提交小于或等于 1950 年的年份时,我们都会正确抛出 ToyCarTooOldException
对象 - 但是我们的 ToyCarValidatorTest
会发生什么情况?
我们修改一下年份小于 1950 年的测试数据,看看会发生什么:
-
使用以下内容修改数据提供者内容:
codebase/symfony/tests/Unit/Validator/ToyCarValidatorTest.phppublic function provideToyCarModel(): array { // Toy Car Color $toyColor = new ToyColor(); $toyColor->setName('White'); // Car Manufacturer $carManufacturer = new CarManufacturer(); $carManufacturer->setName('Williams'); // Toy Car $toyCarModel = new ToyCar(); $toyCarModel->setName(''); // Should fail. $toyCarModel->setColour($toyColor); $toyCarModel->setManufacturer($carManufacturer); $toyCarModel->setYear(1935); return [ [$toyCarModel, ['is_valid' => false, 'name' => false, 'year' => false]], ]; }
-
现在,运行以下命令并查看会发生什么:
/var/www/html/symfony# ./runDebug.sh --filter ToyCarValidatorTest
您会注意到我们测试失败并显示以下消息:
Figure 2. Figure 8.10 – Failed toy car validation现在,我们可以看到有一个未捕获的异常。我们的
ToyCarValidator
程序没有处理这个异常对象的程序。为什么会这样呢?这个例子中的接口是codebase/symfony/src/Validator/ValidatorInterface.php
接口。该接口会抛出一个ToyCarValidationException
对象。现在的问题是,我们的实现类YearValidator.php
类抛出的异常与其合约或接口不同。因此,它破坏了行为。要解决这个问题,我们只需抛出接口中声明的正确异常即可。 -
让我们修改
ToyCarTooOldException
类:codebase/symfony/src/Validator/ToyCarTooOldException.php<?php namespace App\Validator; class ToyCarTooOldException extends ToyCarValidationException { }
如您所见,我们只需将其扩展类替换为
ToyCarValidationException
。ToyCarValidator.php
类就是用来捕捉这种异常的。 -
现在,让我们通过运行以下命令来运行测试,看看它是否真的有效:
/var/www/html/symfony# ./runDebug.sh --filter ToyCarValidatorTest
我们现在应该通过测试并看到以下结果:
Figure 3. Figure 8.11 – Passing the toy car validator test, with old car validation -
现在我们又通过了测试,让我们看看
ToyCarValidator
程序返回了什么。还记得我们在 第 5 章 "单元测试" 中写的 shell 脚本吗?让我们使用其中的一个。在codebase/symfony/tests/Unit/Validator/ ToyCarValidatorTest.php
第 23 行设置断点。然后运行以下命令:/var/www/html/symfony# ./runDebug.sh --filter ToyCarValidatorTest
-
检查
$result
变量,应该可以看到以下内容:Figure 4. Figure 8.12 – Validation model
我们可以看到,ToyCarValidator
的 validate
方法会返回一个 ValidationModel
对象。它给出了我们验证的字段摘要,以及 year
字段的异常消息。
我们已经看到接口是如何发挥作用的,但有时它们也会变得过于强大。接下来,我们将讨论 接口隔离原则 (ISP),以帮助避免这种情况的发生。