PHPUnit 中的模拟 - 具有不同参数的同一方法的多个配置

Posted

技术标签:

【中文标题】PHPUnit 中的模拟 - 具有不同参数的同一方法的多个配置【英文标题】:Mock in PHPUnit - multiple configuration of the same method with different arguments 【发布时间】:2011-07-25 23:17:58 【问题描述】:

可以这样配置phpUnit mock吗?

$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”参数。 就像第二个期望是重写第一个,但是当我转储模拟时,一切看起来都很好。

【问题讨论】:

【参考方案1】:

遗憾的是,默认的 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()));

这可以正常工作,但您的测试超出了应有的范围(主要是它首先使用匹配器调用,这是一个实现细节)。

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


接受两个参数并使用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-&gt;exactly(2) 来表明这也适用于计算调用。如果您不需要将其换成$this-&gt;any(),当然可以。

【讨论】:

很好的解决方案!我同意你的观点,调用顺序不应该泄漏到测试中。但是,使用第二种方法,您无法保证该方法是否使用每个单独的预期值调用一次。也就是说,在您的示例中,代码可以使用Matcher 作为参数调用上下文两次,并且永远不会使用Logger 作为参数,它仍然会通过。取决于可能是问题的测试。在不将顺序泄漏到测试用例中的情况下,您如何解决这个问题? @MarijnHuizendveld “绑定一系列预期调用的闭包,删除它看到的每个人,并在测试结束时删除一个 assertEmpty”将是首先想到的。 请注意 ->at($x) 包括对其他方法的函数调用,所以如果你先模拟另一个方法,不管它是同一个方法还是使用 ->at($x)本身,$x 从 1 而不是 0 开始。 仍然 $this->returnCallback 是唯一的解决方案:/【参考方案2】:

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

【讨论】:

是的,您可以使用 returnValueMap() 但请确保您指定了该方法所需的所有参数,否则它无法正常工作。见github.com/sebastianbergmann/phpunit-mock-objects/issues/89【参考方案3】:

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

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);
    

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

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 做出断言,因此有四个断言。

【讨论】:

【参考方案4】:

根据@edorian 和 cmets (@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->callback 的回调函数作为 with 的参数,而不是遗嘱中的 returnCallback。这失败了,因为内部 phpunit 在验证参数匹配器回调的过程中调用了回调两次。这意味着该方法在第二次调用时失败,该参数已从预期的参数数组中删除。我不知道为什么 phpunit 会调用它两次(似乎是不必要的浪费),我想您可以通过仅在第二次调用时将其删除来解决此问题,但我没有足够的信心认为这是预期的和一致的 phpunit 行为依靠发生的事情。

【讨论】:

非常感谢您花时间打出一个例子!非常有帮助。 :)【参考方案5】:

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

【讨论】:

【参考方案6】:

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

【讨论】:

【参考方案7】:

这里还有一些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();
        
    );

【讨论】:

以上是关于PHPUnit 中的模拟 - 具有不同参数的同一方法的多个配置的主要内容,如果未能解决你的问题,请参考以下文章

PHPUnit:如何使用多个参数模拟多个方法调用?

如何让 PHPUnit MockObjects 根据参数返回不同的值?

phpunit 避免模拟的构造函数参数

模拟框架返回具有不同名称和类型的类

PHPUnit 中的模拟与存根

使用 PHPUnit 模拟 PDO 对象