在 PHP 中使用匿名类测试抽象类
抽象类不能直接实例化,这为测试抽象类本身实现的功能时带来了挑战。在这篇文章中,我将分享我解决这一问题的方法。
为了说明该技术,让我们假设有一个带有 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);
希望这种方法对你有帮助。