🐘 PHP 中的单元测试

2025-06-04

🐘 PHP 中的单元测试

大家好!接下来我将分享一下我使用 PHP 设计单元测试的方法。

免责声明

让我们从(我的?)单元测试的定义开始:

单元测试的目标是检查每个公共函数的正确行为,并提供一组具有代表性的数据。
重要的是,这些函数内部的外部调用必须在专门的测试中进行测试,因此需要进行模拟。
进行这些测试的目的是为了在每次修改代码库时运行它们,以验证没有引入回归。

作为先决条件,代码需要具备可测试性,以便我们能够模拟依赖项。在给定函数中,依赖项可以是内部的(例如,项目的另一个类),也可以是外部的(例如,对第三方库的调用)。

我不会讨论如何让代码可测试。我知道两种使用依赖注入原则实现可测试的技术,但也许还有其他选择:

  • 首先,你可以修改函数的接口,将其所有依赖项作为参数传递(或使用构造函数或 setter),
  • 或者您可以使用依赖注入容器(又名 DIC),它是软件开发中的常见(反?)模式(有关更多详细信息,请参阅PHP 的 PSR11 )。

代码架构

当我开始一个新的 Web 项目时,我通常会将代码分成几个不同的层:

  • 路由器(通常是 Slim路由器),负责处理 HTTP 请求,
  • 控制层负责数据验证和输出渲染,
  • 业务,负责业务逻辑,
  • 映射层(又名 DTO)
  • 模型层(又名DAO)。

这样做使得代码更易于测试。

根据需要,可能还需要一些额外的东西,例如:

  • 检查器,用于验证输入,
  • 视图,用于渲染输出,
  • 中间件,添加 HTTP 请求的预处理和后处理,
  • routes,用于管理 REST API 的路由,
  • 启动器,用于管理对第三方 API(短信、电子邮件等)的传出请求。

最后,一个引导文件用于创建所有内容。

经典的工作流程是:

  • 控制器在给定函数上被调用,
  • 它使用专用检查器验证输入
  • 它使用已验证的输入调用业务层,
    • 业务可选使用映射器与数据库交互
    • 它还可以通过启动器与第三方 API 进行交互
    • 它可选择将模型返回到控制器
  • 控制器使用专用视图呈现业务层返回的模型

当我为特定层编写单元测试时,我只会测试该层函数的行为,并模拟对子层函数的调用。例如,当我为controller编写测试时,我会模拟对business层、checkerview 的调用。当我为业务对象编写测试时,我会模拟对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);
    }
}
Enter fullscreen mode Exit fullscreen mode

这里你可以看到getById函数中有 3 个依赖项。我选择通过类的构造函数传递这些依赖项UserController。或者,我可以使用传递给函数(或构造函数)的容器,或者通过函数的参数传递依赖项。结果是一样的:我必须模拟这 3 个依赖项才能测试该函数。

使用模拟

值得庆幸的是,PHPUnit提供了一套很棒的 AP​​I 来处理模拟对象(参见测试替身)。我不会在这里介绍所有功能,但其文档值得一看。

首先,我需要模拟checkId的函数。正如你可能猜到的那样,如果 的格式错误,UserChecker它会引发异常。 然后,我模拟的函数 最后,模拟 的函数id
getByIdUserBusiness
renderUserView


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);
    }
}
Enter fullscreen mode Exit fullscreen mode

每一层都必须做同样的事情。

结论

编写详尽的单元测试可能会很痛苦,因为大多数情况下,你花在编写测试上的时间比编写“真正”代码的时间还要多。
但在我看来,在测试应用方面,没有任何可接受的权衡。
我知道还有其他测试技术(比如 TDT),但我认为它们是互补测试,因为它们更接近集成测试而不是单元测试。但也许我错了?


请随意发表您的意见!

感谢阅读!

文章来源:https://dev.to/biros/-unit-tests-in-php-3il7
PREV
使用 Tmux 构建自定义 IDE 欢迎使用 tmux!
NEXT
📊 DEV 上的语言流行度