🐘 PHP 中的单元测试
大家好!接下来我将分享一下我使用 PHP 设计单元测试的方法。
免责声明
让我们从(我的?)单元测试的定义开始:
单元测试的目标是检查每个公共函数的正确行为,并提供一组具有代表性的数据。
重要的是,这些函数内部的外部调用必须在专门的测试中进行测试,因此需要进行模拟。
进行这些测试的目的是为了在每次修改代码库时运行它们,以验证没有引入回归。
作为先决条件,代码需要具备可测试性,以便我们能够模拟依赖项。在给定函数中,依赖项可以是内部的(例如,项目的另一个类),也可以是外部的(例如,对第三方库的调用)。
我不会讨论如何让代码可测试。我知道两种使用依赖注入原则实现可测试的技术,但也许还有其他选择:
- 首先,你可以修改函数的接口,将其所有依赖项作为参数传递(或使用构造函数或 setter),
- 或者您可以使用依赖注入容器(又名 DIC),它是软件开发中的常见(反?)模式(有关更多详细信息,请参阅PHP 的 PSR11 )。
代码架构
当我开始一个新的 Web 项目时,我通常会将代码分成几个不同的层:
- 路由器(通常是 Slim路由器),负责处理 HTTP 请求,
- 控制层,负责数据验证和输出渲染,
- 业务层,负责业务逻辑,
- 映射层(又名 DTO),
- 模型层(又名DAO)。
这样做使得代码更易于测试。
根据需要,可能还需要一些额外的东西,例如:
- 检查器,用于验证输入,
- 视图,用于渲染输出,
- 中间件,添加 HTTP 请求的预处理和后处理,
- routes,用于管理 REST API 的路由,
- 启动器,用于管理对第三方 API(短信、电子邮件等)的传出请求。
最后,一个引导文件用于创建所有内容。
经典的工作流程是:
- 控制器在给定函数上被调用,
- 它使用专用检查器验证输入,
- 它使用已验证的输入调用业务层,
- 业务层可选使用映射器与数据库交互
- 它还可以通过启动器与第三方 API 进行交互
- 它可选择将模型返回到控制器层
- 控制器使用专用视图来呈现业务层返回的模型。
当我为特定层编写单元测试时,我只会测试该层函数的行为,并模拟对子层函数的调用。例如,当我为controller编写测试时,我会模拟对business层、checker和view 的调用。当我为业务对象编写测试时,我会模拟对mapper、 enabler和其他业务对象的调用。
例子
让我们通过一些代码来看一下控制器的样子:
class UserController
{
/** @var UserBusiness */
public $business;
/** @var UserChecker */
public $checker;
/** @var UserView */
public $view;
public function __construct(
UserBusiness $business,
UserChecker $checker,
UserView $view
) {
$this->business = $business;
$this->checker = $checker;
$this->view = $view;
}
public function getById(string $id): string
{
$this->checker->checkId($id);
$user = $this->business->getById($id);
return $this->view->render($user);
}
}
这里你可以看到getById
函数中有 3 个依赖项。我选择通过类的构造函数传递这些依赖项UserController
。或者,我可以使用传递给函数(或构造函数)的容器,或者通过函数的参数传递依赖项。结果是一样的:我必须模拟这 3 个依赖项才能测试该函数。
使用模拟
值得庆幸的是,PHPUnit提供了一套很棒的 API 来处理模拟对象(参见测试替身)。我不会在这里介绍所有功能,但其文档值得一看。
首先,我需要模拟checkId
的函数。正如你可能猜到的那样,如果 的格式错误,UserChecker
它会引发异常。 然后,我模拟的函数。 最后,模拟 的函数。id
getById
UserBusiness
render
UserView
class UserControllerTest extends PHPUnit\Framework\TestCase
{
public function testGetById_Ok()
{
$business = $this->createMock(UserBusiness::class);
$checker = $this->createMock(UserChecker::class);
$view = $this->createMock(UserView::class);
$expectedId = 'id';
$expectedUser = new UserModel($expectedId, 'john', 'doe');
$expectedResult = 'result';
$checker->expects($this->once())
->method('checkId')
->with($expectedId);
$business->expects($this->once())
->method('getById')
->with($expectedId)
->willReturn($expectedUser);
$view->expects($this->once())
->method('render')
->with($expectedUser)
->willReturn($expectedResult);
$controller = new UserController($business, $checker, $view);
$actualResult = $controller->getById($expectedId);
$this->assertEquals($expectedResult, $actualResult);
}
}
每一层都必须做同样的事情。
结论
编写详尽的单元测试可能会很痛苦,因为大多数情况下,你花在编写测试上的时间比编写“真正”代码的时间还要多。
但在我看来,在测试应用方面,没有任何可接受的权衡。
我知道还有其他测试技术(比如 TDT),但我认为它们是互补测试,因为它们更接近集成测试而不是单元测试。但也许我错了?
请随意发表您的意见!
感谢阅读!
文章来源:https://dev.to/biros/-unit-tests-in-php-3il7