PHP 单元测试 - 使用 Mockery 模拟静态自动加载类

Posted

技术标签:

【中文标题】PHP 单元测试 - 使用 Mockery 模拟静态自动加载类【英文标题】:PHP Unit Testing - Mock static autoloaded class with Mockery 【发布时间】:2017-09-26 11:50:40 【问题描述】:

我想对官方 Segment php 集成的包装类进行单元测试。因此,我必须用 Mockery 模拟 the Segment class,这样就不会有任何真正的 API 请求。

问题

要模拟的类仅包含静态方法。因此,我尝试像这样模拟它(使用alias):

$segment = Mockery::mock('alias:Segment');

这可行,但前提是类不是由作曲家自动加载的。如果我加载它 - 就像我必须为应用程序的其余部分加载一样 - 我会收到错误

Could not load mock Segment, class already exists.

(这是有道理的,因为文档声明之前不得加载别名类。)

问题

我怎样才能模拟这个(邪恶的?)类,但仍然像往常一样在我的应用程序的其余部分使用它?

【问题讨论】:

如果我理解正确的话,这听起来更像是一个设计问题,而不是“如何模拟它”的问题。您引用该库的类到底是什么样的?您是以某种方式注入依赖项还是只是直接从您的方法中直接调用库? 我也想过这个问题,但想不出解决办法。目前,我将这些方法直接称为:Segment::track(...)。注入类没有意义,因为它只有静态方法,不是吗? 我认为这很有意义,因为这将允许您从测试中通过它的模拟版本。这在生产中似乎没有必要,因为它需要为您的类进行额外设置,但我认为这是使类可测试的常用方法。如果你有能力使用依赖注入容器(如PHP-DI),那将不是问题。 是的,额外的包装器也是一个很好的解决方案,但是您仍然必须测试该包装器并遇到同样的问题。此外,您的类已经 一个包装器,所以我认为这只是包装器-ception :) 这样想:您要测试的只是调用某个对象上的特定方法。做到这一点的最好方法是控制该对象是什么,并允许这样做,您只需从外部传入它,而不是在您的类中自动加载它。然后在生产环境中通过真实类,在测试用例中通过模拟。 嗯,我明白了。但这意味着我将注入 Segment 类的实例,然后在其上调用静态方法,例如 $segmentInstance->track(...)。我认为这是不好的做法,因为它“隐藏”了我在静态上下文中调用track(...) 【参考方案1】:

本质上,您不能使用静态调用模拟类。

静态调用总是准确地引用要调用的类和方法,这相当于准确地指向要执行的文件和代码行(如果您假设基本的自动加载功能可用)。

执行不同代码的唯一方法是不包含原始类,而是首先加载模拟类代码。如果您有替代代码文件,或者您是否使用 Mockery 调用 eval(),则无关紧要。两种方式都可以。

但它们也只能工作一次。您不能在以后的测试中切换回原始代码,因为每次脚本运行只能定义一次该类。并且无法切换实现(例如原始与模拟与另一个模拟)是这里的问题。

在 cmets 中也提到了解决方案:不要有具有静态方法的类。始终创建类的实例并调用动态方法。这样您就可以轻松地模拟类,但它需要先创建一个实例,并提供一种将类(或至少是模拟的类)注入到您要测试的代码中的方法。

作为一种通用模式,如果项目中没有依赖注入,我将使用此模式(有时我必须处理一些遗留问题):

public function __construct(MyClass $class = null) 
    $this->class = $class ?: new MyClass();

这样我就不必注入类,但我可以注入一个模拟而不是真正的类。

对于依赖注入可用的情况,构造函数将是一个非常基本的初始化器:

public function __construct(MyClass $class) 
    $this->class = $class;

如果您的依赖注入框架能够进行自动连接(如 PHP-DI),并且您只有一个 MyClass,这将非常有用,因为它会自动注入而无需您定义任何东西。

【讨论】:

谢谢斯文!在我自己的代码中,我尽可能使用依赖注入,但在这种情况下,我必须模拟一个官方包(Segment PHP Integration)。这个包是以完全静态的方式编写的,所以我一直在寻找一种模拟这个类的方法——我无法改变它。 看图书馆,有几个细节不喜欢。但是,您可以通过多种方式处理这种情况。 1.您可以忽略将所有调用传递给内部实例化对象的静态类,并自己做同样的事情。 2. 你可以用一个实例化的包装器来包装静态类,然后你可以模拟它。 谢谢,很高兴知道没有比这更简单的方法了。

以上是关于PHP 单元测试 - 使用 Mockery 模拟静态自动加载类的主要内容,如果未能解决你的问题,请参考以下文章

在 Laravel(流明)上使用 Mockery 模拟 Eloquent 模型不起作用

使用 Mockery Eloquent 模型模拟 Laravel

错误:在 null 上调用成员函数 create(),单元测试(嘲弄)

如何使用 mockery 来模拟没有命名空间的全局类?

使用 Mockery 进行单元测试返回 false

mockery->shouldReceive() 啥时候不应该通过?