【问题标题】:Mock in PHPUnit - multiple configuration of the same method with different argumentsPHPUnit 中的模拟 - 具有不同参数的同一方法的多个配置
【发布时间】: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”参数。 就像第二个期望是重写第一个,但是当我转储模拟时,一切看起来都很好。

【问题讨论】:

    标签: php mocking phpunit


    【解决方案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() 可用于根据给定的方法存根参数返回不同的值。

    【讨论】:

    【解决方案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();
                  }
              });
          

          【讨论】:

            猜你喜欢
            • 2014-05-05
            • 1970-01-01
            • 2021-03-07
            • 2019-01-27
            • 2010-12-16
            • 2021-11-01
            • 2015-10-03
            • 2013-09-12
            • 2017-09-06
            相关资源
            最近更新 更多