编程

在 PHP 中使用匿名类测试抽象类

573 2023-12-20 22:14:00

抽象类不能直接实例化,这为测试抽象类本身实现的功能时带来了挑战。在这篇文章中,我将分享我解决这一问题的方法。

为了说明该技术,让我们假设有一个带有 move() 方法的抽象类 Vehicle,同时在其子类中强制实现 speed() 方法。

/app/Utils/Vehicle.php

namespace App\Utils;

use Exception;

abstract class Vehicle
{
    abstract protected function speed(): float;

    /**
     * @throws Exception
     */
    public function move(float $distance): float
    {
        $speed = $this->speed();

        if ($speed <= 0) {
            throw new Exception('Vehicle does not move. Speed 0.');
        }

        return round($distance / $speed, 2);
    }
}

理想情况下,我们的目标是 move() 方法编写单个测试用例,使用不同场景的数据集运行。虽然可以在每个子类的测试中额外测试该方法,但这里并不是重点。

现在,我们测试 move() 方法有什么选项?一种方法是创建类的部分模拟,模拟该 protected 的 speed() 方法。然而,我个人尽量避免在测试中随意在任意地方进行测试。虽然在这种简单的情况下,部分模拟可能是可以接受的,但根据我的经验,随着代码的发展,大量使用模拟可能会导致问题。此外,我们需要模拟的 speed() 方法是 protected 的。尽管 Mockery 可以模拟 proctected 的方法,但需要显式权限。值得注意的是,Mockery 文档明确建议不要采用这种做法。因此,我尽量避免模拟 protected 的方法。

另一种选择是使用匿名类,在测试用例中扩展抽象类。顺便说一句,我将 PEST 用于下面的测试用例。

/tests/Unit/Utils/VehicleTest.php

namespace Tests\Unit\Utils;

use App\Utils\Vehicle;

it('calculates the duration it will take to move the distance', function ($speed, $distance, $duration) {
    $vehicle = new class ($speed) extends Vehicle {
        public function __construct(private float $speed) {}

        protected function speed(): float
        {
            return $this->speed;
        }
    };

    expect($vehicle->move($distance))->toBe($duration);
})->with([
    [60, 60, 1.0],
    [45.5, 87.3, 1.92],
    [310, 100, 0.32],
]);

匿名类可以当场编写整个类定义,而不是对其他地方定义的类使用 new Something()。在我们的匿名存根类中,speed() 方法的返回值作为提升的构造函数参数传递。

好的,现在让我们为速度为零或更低的场景添加另一个测试用例,使得 move() 方法抛出异常。

it('throws an exception when the return value of the speed method is zero or below', function ($speed) {
    $vehicle = new class ($speed) extends Vehicle {
        public function __construct(private float $speed) {}

        protected function speed(): float
        {
            return $this->speed;
        }
    };

    $vehicle->move(123);
})->with([0, -0.1, -1])->throws(Exception::class);

由于创建匿名类的实例在两个测试用例中的工作方式相同,我们可以将其提取到辅助函数中。

/tests/Unit/Utils/VehicleTest.php

namespace Tests\Unit\Utils;

use App\Utils\Vehicle;

function getVehicleWithSpeed(float $speed): Vehicle
{
    return new class ($speed) extends Vehicle {
        public function __construct(private float $speed) {}

        protected function speed(): float
        {
            return $this->speed;
        }
    };
}

it('calculates the duration it will take to move the distance', function ($speed, $distance, $duration) {
    expect(getVehicleWithSpeed($speed)->move($distance))->toBe($duration);
})->with([
    [60, 60, 1.0],
    [45.5, 87.3, 1.92],
    [310, 100, 0.32],
]);

it('throws an exception when the return value of the speed method is zero or below', function ($speed) {
    getVehicleWithSpeed($speed)->move(123);
})->with([0, -0.1, -1])->throws(Exception::class);

希望这种方法对你有帮助。