使用里氏替换原则的 TDD

LSP 由 Barbara Liskov 提出。我使用它的方式是,一个接口的实现应该可以用该接口的另一个实现来替代,而不会改变行为。如果您要扩展一个超类,那么子类必须能够在不破坏行为的情况下替换超类。

在本例中,让我们尝试添加一条业务规则,拒绝接受 1950 年或之前生产的玩具汽车模型。

像往常一样,让我们从测试开始:

  1. 打开我们之前创建的 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],
            ];
        }
    }
  2. 我们添加了一个新测试,以便检查 ToyCarTooOldException。我们也添加这个异常类,但首先,让我们运行测试。

  3. 运行以下命令:

    /var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter testCanRejectVeryOldCar
  4. 现在您将看到四个错误。没关系。现在,让我们添加缺少的异常类:

    codebase/symfony/src/Validator/ToyCarTooOldException.php
    <?php
    
    namespace App\Validator;
    
    class ToyCarTooOldException extends \Exception
    {
    
    }

    如你所见,这只是一个简单的异常类,它扩展了主要的 PHP \Exception 类。

    如果我们再次运行测试,现在应该可以通过测试了,因为我们已经通过使用 $this→expectException() 方法告诉 PHPUnit,我们期待在此测试中出现异常。

  5. 运行以下命令:

    /var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter testCanRejectVeryOldCar

现在,我们应该能够通过测试 - 您应该看到以下结果:

image 2023 10 24 12 02 13 961
Figure 1. Figure 8.9 – Passing the old car rejection test

这意味着每当我们提交小于或等于 1950 年的年份时,我们都会正确抛出 ToyCarTooOldException 对象 - 但是我们的 ToyCarValidatorTest 会发生什么情况?

我们修改一下年份小于 1950 年的测试数据,看看会发生什么:

  1. 使用以下内容修改数据提供者内容:

    codebase/symfony/tests/Unit/Validator/ToyCarValidatorTest.php
    public 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]],
        ];
    }
  2. 现在,运行以下命令并查看会发生什么:

    /var/www/html/symfony# ./runDebug.sh --filter ToyCarValidatorTest

    您会注意到我们测试失败并显示以下消息:

    image 2023 10 24 13 13 06 279
    Figure 2. Figure 8.10 – Failed toy car validation

    现在,我们可以看到有一个未捕获的异常。我们的 ToyCarValidator 程序没有处理这个异常对象的程序。为什么会这样呢?这个例子中的接口是 codebase/symfony/src/Validator/ValidatorInterface.php 接口。该接口会抛出一个 ToyCarValidationException 对象。现在的问题是,我们的实现类 YearValidator.php 类抛出的异常与其合约或接口不同。因此,它破坏了行为。要解决这个问题,我们只需抛出接口中声明的正确异常即可。

  3. 让我们修改 ToyCarTooOldException 类:

    codebase/symfony/src/Validator/ToyCarTooOldException.php
    <?php
    
    namespace App\Validator;
    
    class ToyCarTooOldException extends ToyCarValidationException
    {
    
    }

    如您所见,我们只需将其扩展类替换为 ToyCarValidationExceptionToyCarValidator.php 类就是用来捕捉这种异常的。

  4. 现在,让我们通过运行以下命令来运行测试,看看它是否真的有效:

    /var/www/html/symfony# ./runDebug.sh --filter ToyCarValidatorTest

    我们现在应该通过测试并看到以下结果:

    image 2023 10 24 13 16 29 819
    Figure 3. Figure 8.11 – Passing the toy car validator test, with old car validation
  5. 现在我们又通过了测试,让我们看看 ToyCarValidator 程序返回了什么。还记得我们在 第 5 章 "单元测试" 中写的 shell 脚本吗?让我们使用其中的一个。在 codebase/symfony/tests/Unit/Validator/ ToyCarValidatorTest.php 第 23 行设置断点。然后运行以下命令:

    /var/www/html/symfony# ./runDebug.sh --filter ToyCarValidatorTest
  6. 检查 $result 变量,应该可以看到以下内容:

    image 2023 10 24 13 18 13 335
    Figure 4. Figure 8.12 – Validation model

我们可以看到,ToyCarValidatorvalidate 方法会返回一个 ValidationModel 对象。它给出了我们验证的字段摘要,以及 year 字段的异常消息。

我们已经看到接口是如何发挥作用的,但有时它们也会变得过于强大。接下来,我们将讨论 接口隔离原则 (ISP),以帮助避免这种情况的发生。