如何使用PHPUnit测试来模拟方法链。

10

我正在尝试模拟一个链式(嵌套)方法来返回所需的值,这是代码:

public function __construct($db)
{
$this->db = $db;
}

public function getResults()
{
return $this->db->getFinder()->find($this->DBTable);
}

我尝试使用这个模拟,但它并不起作用:

$dbMock = $this->createMock(DB::class);
        $dbMock = $dbMock
            ->expects(self::any())
            ->method('getFinder')
            ->method('find')
            ->with('questions')
            ->will($this->returnValue('7'));

有没有解决这个问题的方案?

谢谢。

3个回答

12

使用Mocking Demeter Chains And Fluent Interfaces现在更加简单了

简单

$dbMock = $dbMock
        ->expects(self::any())
        ->method('getFinder->find')
        ->with('questions')
        ->will($this->returnValue('7'));

mockery文档中的另一个示例

$object->foo()->bar()->zebra()->alpha()->selfDestruct();

如果您想让 selfDestruct 返回 10,则需执行以下操作:

$mock = \Mockery::mock('CaptainsConsole');
$mock->shouldReceive('foo->bar->zebra->alpha->selfDestruct')->andReturn(10);

5

链式调用是指对象按顺序被调用的过程。因此,您需要实现一条模拟链。只需以一种能够返回模拟对象的方式模拟方法即可。

类似以下代码应该可以实现:

$finderMock = $this->createMock(Finder::class);
$finderMock = $finderMock
    ->expects(self::any)
    ->method('find')
    ->with('questions')
    ->will($this->returnValue('7'));

$dbMock = $this->createMock(DB::class);
$dbMock = $dbMock
    ->expects(self::any())
    ->method('getFinder')
    ->will($this->returnValue($finderMock));

在这篇很棒的博客文章中了解更多有关模拟链的信息

虽然如此,我并不真正看到测试链的意义。在我看来,最好将测试限制为一次测试1个模块(函数)或2个模块(交互)。


谢谢,但是当我尝试这个:http://pastie.org/private/cpxfphhjbl2y4ih7iudjra时它没有起作用,我得到了这个错误: - Asem Khatib
PHP致命错误:在/var/www/public/mypoll/tests/unit/classes/PaginationTest.php的第56行调用未定义的方法PHPUnit_Framework_MockObject_Builder_InvocationMocker :: getFinder()。 - Asem Khatib
@ebncana 这很奇怪... 你能在“Pagination”构造函数内部进行调试,检查你得到的对象是什么吗? - BVengerov
我无法从CLI运行调试,但这是分页类:http://pastie.org/private/lcd33hz5et32iu0amzlsqw - Asem Khatib
@ebncana 它不能工作的事情让我感到困扰... 如果你尝试以下代码会发生什么?(http://pastie.org/10945375) - BVengerov

3
尽管 @BVengerov 的回答肯定是有效的,但我建议改变设计。我认为链式mock不是正确的方法,它会影响可读性和测试的简单性。
我建议将 Finder 类作为您的类的成员。因此,您现在只需mock出 Finder 即可。
class MyClass {

    private $finder;

    public function __construct(Finder $finder) {
        $this->finder = $finder;
    }

    public function getResults() {
        return $this->finder->find($this->DBTable);
    }
}

这个更改使得测试这个函数(和类!)变得简单
“但是我需要在类的其他地方使用$db变量!”首先,这可能表明您当前的类中有一个类渴望被提取出来。保持类小而简单。
然而,作为一种快速而肮脏的解决方案,请考虑添加一个setFinder() setter,仅供测试使用。

谢谢@Pete,但是每次将RedBeanPHP的finder和toolbox注入到每个类中以便能够正确测试它将会非常痛苦,请查看最后一次提交:https://github.com/AsemKhatib/MyPoll-OOP-PHP-Poll-system/commit/709bddfd2fcd8fcbbc1ca8cceb6d11585131fd68 所以我需要在工厂中的每个实例中注入它们两个:https://github.com/AsemKhatib/MyPoll-OOP-PHP-Poll-system/blob/testing/classes/factory.class.php 而不是只注入整个db类。我真的很困惑。 - Asem Khatib
你的Factory是“服务定位器”模式的实现(例如,所有类都接收定位器并从中检索它们实际需要的服务)。最简单的解决方案可能是模拟定位器(工厂)。但是,这需要您确切地知道被测试类需要哪些依赖项(这就是为什么有些人认为这是一种反模式的原因)。您是否考虑过使用诸如PHP-DI之类的依赖注入框架? - Pieter van den Ham
请注意,这样的框架并非必需,但它可以解决依赖注入的“非常痛苦”的方面,而无需使用框架。 - Pieter van den Ham
谢谢@Pete,我会在移除工厂后尝试实现DI,并告诉你发生了什么,非常感谢。 - Asem Khatib
@Pete,我不明白。但现在测试链式方法的问题已经转移到另一个类中了。现在他需要在Finder类中对这些链进行单元测试。我错了吗? - user6827096

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接