PHPUnit中的模拟 - 使用不同参数的相同方法的多个配置

时间:2011-03-30 09:39:34

标签: php mocking phpunit

是否可以用这种方式配置PHPUnit?

$context = $this->getMockBuilder('Context')
   ->getMock();

$context->expects($this->any())
   ->method('offsetGet')
   ->with('Matcher')
   ->will($this->returnValue(new Matcher()));

$context->expects($this->any())
   ->method('offsetGet')
   ->with('Logger')
   ->will($this->returnValue(new Logger()));

我使用PHPUnit 3.5.10,当我要求Matcher时它会失败,因为它需要“Logger”参数。 这就像第二个期望是重写第一个,但是当我转储模拟时,一切看起来都不错。

7 个答案:

答案 0 :(得分:60)

遗憾的是,默认的PHPUnit Mock API无法实现这一点。

我可以看到两个可以让你接近这样的选项:

使用 - > at($ x)

$context = $this->getMockBuilder('Context')
   ->getMock();

$context->expects($this->at(0))
   ->method('offsetGet')
   ->with('Matcher')
   ->will($this->returnValue(new Matcher()));

$context->expects($this->at(1))
   ->method('offsetGet')
   ->with('Logger')
   ->will($this->returnValue(new Logger()));

这样可以正常工作,但是你测试的比你应该的多(主要是先用matcher调用它,这是一个实现细节)。

如果你对每个函数有多个调用,这也会失败!


接受这两个参数并使用returnCallBack

这是更多的工作,但由于您不依赖于呼叫的顺序,因此效果更好:

工作示例:

<?php

class FooTest extends PHPUnit_Framework_TestCase {


    public function testX() {

        $context = $this->getMockBuilder('Context')
           ->getMock();

        $context->expects($this->exactly(2))
           ->method('offsetGet')
           ->with($this->logicalOr(
                     $this->equalTo('Matcher'), 
                     $this->equalTo('Logger')
            ))
           ->will($this->returnCallback(
                function($param) {
                    var_dump(func_get_args());
                    // The first arg will be Matcher or Logger
                    // so something like "return new $param" should work here
                }
           ));

        $context->offsetGet("Matcher");
        $context->offsetGet("Logger");


    }

}

class Context {

    public function offsetGet() { echo "org"; }
}

这将输出:

/*
$ phpunit footest.php
PHPUnit 3.5.11 by Sebastian Bergmann.

array(1) {
  [0]=>
  string(7) "Matcher"
}
array(1) {
  [0]=>
  string(6) "Logger"
}
.
Time: 0 seconds, Memory: 3.00Mb

OK (1 test, 1 assertion)

我在匹配器中使用$this->exactly(2)来表明这也适用于计算调用。如果您不需要将其交换为$this->any(),那么当然可以。

答案 1 :(得分:29)

从PHPUnit 3.6开始,有$this->returnValueMap()可用于根据方法存根的给定参数返回不同的值。

答案 2 :(得分:7)

您可以通过回调实现此目的:

class MockTest extends PHPUnit_Framework_TestCase
{
    /**
     * @dataProvider provideExpectedInstance
     */
    public function testMockReturnsInstance($expectedInstance)
    {
        $context = $this->getMock('Context');

        $context->expects($this->any())
           ->method('offsetGet')
           // Accept any of "Matcher" or "Logger" for first argument
           ->with($this->logicalOr(
                $this->equalTo('Matcher'),
                $this->equalTo('Logger')
           ))
           // Return what was passed to offsetGet as a new instance
           ->will($this->returnCallback(
               function($arg1) {
                   return new $arg1;
               }
           ));

       $this->assertInstanceOf(
           $expectedInstance,
           $context->offsetGet($expectedInstance)
       );
    }
    public function provideExpectedInstance()
    {
        return array_chunk(array('Matcher', 'Logger'), 1);
    }
}

应该传递给Context Mock的offsetGet方法的任何“Logger”或“Matcher”参数:

F:\Work\code\gordon\sandbox>phpunit NewFileTest.php
PHPUnit 3.5.13 by Sebastian Bergmann.

..

Time: 0 seconds, Memory: 3.25Mb

OK (2 tests, 4 assertions)

如您所见,PHPUnit运行了两个测试。每个dataProvider值一个。在每个测试中,它都为with()做出了断言,为instanceOf做出了断言,因此有四个断言。

答案 3 :(得分:5)

继续@edorian的回答和评论(@MarijnHuizendveld)关于确保用Matcher和Logger调用方法,而不是简单地用Matcher或Logger调用两次,这是一个例子。

$expectedArguments = array('Matcher', 'Logger');
$context->expects($this->exactly(2))
       ->method('offsetGet')
       ->with($this->logicalOr(
                 $this->equalTo('Matcher'), 
                 $this->equalTo('Logger')
        ))
       ->will($this->returnCallback(
            function($param) use (&$expectedArguments){
                if(($key = array_search($param, $expectedArguments)) !== false) {
                    // remove called argument from list
                    unset($expectedArguments[$key]);
                }
                // The first arg will be Matcher or Logger
                // so something like "return new $param" should work here
            }
       ));

// perform actions...

// check all arguments removed
$this->assertEquals(array(), $expectedArguments, 'Method offsetGet not called with all required arguments');

这是PHPUnit 3.7。

如果您正在测试的方法实际上没有返回任何内容,并且您只需要测试使用正确的参数调用它,则应用相同的方法。对于这种情况,我还尝试使用$ this-&gt;回调函数的回调函数作为with的参数,而不是遗嘱中的returnCallback。这失败了,因为内部phpunit在验证参数匹配器回调的过程中调用了回调两次。这意味着该方法在第二次调用时失败,该参数已从预期的arguments数组中删除。我不知道为什么phpunit调用它两次(似乎是不必要的浪费),我猜你可以通过在第二次调用时删除它来解决这个问题,但我没有足够的信心这是有意和一致的phpunit行为依靠那种情况发生。

答案 4 :(得分:2)

我偶然发现这个PHP扩展模拟对象:https://github.com/etsy/phpunit-extensions/wiki/Mock-Object

答案 5 :(得分:2)

我对主题的2美分:在使用at($ x)时要注意:这意味着预期的方法调用将是模拟对象上的($ x + 1)方法调用;这并不意味着将是预期方法的($ x + 1)调用。这让我浪费了一些时间,所以我希望它不会和你在一起。亲切的问候每个人。

答案 6 :(得分:0)

还有doublit库的一些解决方案:

解决方案1:使用Stubs::returnValueMap

/* Get a dummy double instance  */
$double = Doublit::dummy_instance(Context::class);

/* Test the "offsetGet" method */
$double::_method('offsetGet')
    // Test that the first argument is equal to "Matcher" or "Logger"
    ->args([Constraints::logicalOr('Matcher', 'Logger')])
    // Return "new Matcher()" when first argument is "Matcher"
    // Return "new Logger()" when first argument is "Logger"
    ->stub(Stubs::returnValueMap([['Matcher'], ['Logger']], [new Matcher(), new Logger()]));

解决方案2:使用回调

/* Get a dummy double instance  */
$double = Doublit::dummy_instance(Context::class);

/* Test the "offsetGet" method */
$double::_method('offsetGet')
    // Test that the first argument is equal to "Matcher" or "Logger"
    ->args([Constraints::logicalOr('Matcher', 'Logger')])
    // Return "new Matcher()" when first argument $arg is "Matcher"
    // Return "new Logger()" when first argument $arg is "Logger"
    ->stub(function($arg){
        if($arg == 'Matcher'){
            return new Matcher();
        } else if($arg == 'Logger'){
            return new Logger();
        }
    });