支持链式调用的模拟对象

7

我想知道是否有一种简洁的方式来模拟支持方法链的对象...例如,数据库查询对象可能具有如下所示的方法调用:

$result = $database->select('my_table')->where(array('my_field'=>'a_value'))->limit(1)->execute();

问题在于,如果我必须模拟两个不同的选择查询以返回不同的结果。有什么想法吗?
这是关于PHPUnit的特定问题,但其他单元测试框架的经验也会有所帮助。

请问您是否可以澄清您想要模拟对象的方式,例如找出它是否被调用或存根方法调用的返回值。换句话说,请解释您尝试使用测试替身的目的。 - Gordon
@Gordon 抱歉,我倾向于交替使用模拟和存根这两个术语。这是一个坏习惯。在我的整个测试套件中,我想要同时进行两者。因此,在这个例子中,我可能会存根选择查询的返回值,但模拟插入操作。如果您对其中一种有建议,那将非常有帮助。谢谢。 - Nathan MacInnes
抱歉,我仍然不完全理解您想要做什么。能否请您展示一下测试用例? - Gordon
@Gordon 这里有点复杂,不太好在这里解释(因此提供了示例),但我正在测试的对象使用另一个具有类似接口的对象来进行查询。我想要模拟/存根第二个对象,以便我对第一个对象的测试不依赖于它。 - Nathan MacInnes
3个回答

14

我不确定这是否是您要找的,所以请留下评论:

class StubTest extends PHPUnit_Framework_TestCase
{
    public function testChainingStub()
    {
        // Creating the stub with the methods to be called
        $stub = $this->getMock('Zend_Db_Select', array(
            'select', 'where', 'limit', 'execute'
        ), array(), '', FALSE);

        // telling the stub to return a certain result on execute
        $stub->expects($this->any())
             ->method('execute')
             ->will($this->returnValue('expected result'));

        // telling the stub to return itself on any other calls
        $stub->expects($this->any())
             ->method($this->anything())
             ->will($this->returnValue($stub));

        // testing that we can chain the stub
        $this->assertSame(
            'expected result',
            $stub->select('my_table')
                 ->where(array('my_field'=>'a_value'))
                 ->limit(1)
                 ->execute()
        );
    }
}

你可以将此与期望值结合使用:

class StubTest extends PHPUnit_Framework_TestCase
{
    public function testChainingStub()
    {
        // Creating the stub with the methods to be called
        $stub = $this->getMock('Zend_Db_Select', array(
            'select', 'where', 'limit', 'execute'
        ), array(), '', FALSE);

        // overwriting stub to return something when execute is called
        $stub->expects($this->exactly(1))
             ->method('execute')
             ->will($this->returnValue('expected result'));

        $stub->expects($this->exactly(1))
             ->method('limit')
             ->with($this->equalTo(1))
             ->will($this->returnValue($stub));

        $stub->expects($this->exactly(1))
             ->method('where')
             ->with($this->equalTo(array('my_field'=>'a_value')))
             ->will($this->returnValue($stub));

        $stub->expects($this->exactly(1))
             ->method('select')
             ->with($this->equalTo('my_table'))
             ->will($this->returnValue($stub));

        // testing that we can chain the stub
        $this->assertSame(
            'expected result',
            $stub->select('my_table')
                 ->where(array('my_field'=>'a_value'))
                 ->limit(1)
                 ->execute()
        );
    }
}

但是如果我在一个测试用例中进行两个不同的查询怎么办?这就变得困难了。 - Nathan MacInnes
@Nathan 为什么?只需更改存根以从执行返回不同的结果即可。 - Gordon
1
@Nathan:你可以使用 $this->at($index) 来从多次调用 execute() 中返回不同的值。不幸的是,这里的 $index 计算的是对同一个模拟对象(如 select()where() [所有模拟对象?我忘了])的所有方法调用,所以正确设置它们可能会非常麻烦。你最好编写一个自定义存根类。 - David Harkness
当我尝试这样做时,我不断收到“在非对象上调用成员函数”的调用。大多数链接的函数也是静态函数,这会有问题吗? - CMCDragonkai
@CMCDragonkai 是的,请参见 https://dev59.com/rHE95IYBdhLWcg3wV8W_ - Gordon
显示剩余2条评论

1

我知道这是一个老问题,但它可能会对未来的谷歌搜索者有所帮助。

我也曾经遇到过在寻找一个能够提供简单易用的语法来模拟和存根方法链的框架方面的问题。然后我决定编写一个简单易用的模拟库。

使用示例:

 // Creating a new mock for SimpleClassForMocking
 $mock = ShortifyPunit::mock('SimpleClassForMocking');

  ShortifyPunit::when($mock)->first_method()
                            ->second_method(2,3)->returns(1);

  ShortifyPunit::when($mock)->first_method()
                            ->second_method(2,3,4)->returns(2);

  ShortifyPunit::when($mock)->first_method(1)
                            ->second_method(2,3,4)->returns(3);

  ShortifyPunit::when($mock)->first_method(1,2,3)
                            ->second_method(1,2)->third_method()->returns(4);

  $mock->first_method()->second_method(2,3); // returns 1
  $mock->first_method()->second_method(2,3,4); // returns 2
  $mock->first_method(1)->second_method(2,3,4); // returns 3
  $mock->first_method(1,2,3)->second_method(1,2)->third_method(); // return 4

GitHub:

https://github.com/danrevah/ShortifyPunit#stubbing-method-chanining


0

这可能不是你想要的答案,但我几年前写了一个模拟对象框架,可以很好地处理这种“取决于输入”的断言:

http://code.google.com/p/yaymock/

http://code.google.com/p/yaymock/wiki/Expectations

我写这个是为了在支持 Swift Mailer 的单元测试中使用,但是据我所知它并没有被其他项目广泛采用。其目的是提供比 PHPUnit 和 SimpleTest 提供的模拟对象调用更好的控制和内省。


谢谢。我会看一下,但我不确定我是否想再使用另一个模拟框架(我刚刚快完成从SimpleTest迁移我的现有测试)。 - Nathan MacInnes
说实话,如果可能会有其他开发人员在同一段代码上工作,并且不想让他们花一天时间来弄清楚“另一个”模拟对象框架,我也可能会坚持使用更广泛使用的东西;) 我当然不依恋它,但它确实很好地完成了它所编写的工作,所以我想无论如何都要提一下它。 - d11wtq

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