【问题标题】:Can I change a method on a PHPUnit Mock after I've set it?设置 PHPUnit Mock 后可以更改方法吗?
【发布时间】:2012-11-17 21:14:28
【问题描述】:

我正在尝试在 setUp 中创建一个模拟实例,并为所有被覆盖的方法提供默认值,然后在几个不同的测试中根据我正在测试的内容更改某些方法的返回值,而无需设置整个模拟。有没有办法做到这一点?

这是我尝试过的,但天真的方法不起作用。该方法仍然返回原始期望设置的值。

第一次设置:

$my_mock->expects($this->any())
        ->method('one_of_many_methods')
        ->will($this->returnValue(true));

在另一个断言之前的另一个测试中:

$my_mock->expects($this->any())
        ->method('one_of_many_methods')
        ->will($this->returnValue(false));

重复这个问题:PHPUnit Mock Change the expectations later,但那个人没有得到任何回应,我认为一个新问题可能会引发这个问题。

【问题讨论】:

  • AFAIK 不幸的是,phpunit 没有这种可能性。例如,您可以使用 $my_mock->__phpunit_hasMatchers(),但这并不是您想要的。当然,您可以使用a)“at”匹配器或b)“returnCallback”在同一方法上设置不同的返回值,但它们取决于a)调用顺序b)调用参数..但也不是您想要的。我会让你知道我发现了一些新的东西。

标签: php mocking phpunit


【解决方案1】:

如果您多次使用相同的方法,您应该使用“at”声明,并在代码中执行正确的计数。这样 PHPUnit 就知道你指的是哪一个,并且可以正确地满足期望/断言。

以下是一个通用示例,其中多次使用方法“run”:

public function testRunUsingAt()
    {
        $test = $this->getMock('Dummy');

        $test->expects($this->at(0))
            ->method('run')
            ->with('f', 'o', 'o')
            ->will($this->returnValue('first'));

        $test->expects($this->at(1))
            ->method('run')
            ->with('b', 'a', 'r')
            ->will($this->returnValue('second'));

        $test->expects($this->at(2))
            ->method('run')
            ->with('l', 'o', 'l')
            ->will($this->returnValue('third'));

        $this->assertEquals($test->run('f', 'o', 'o'), 'first');
        $this->assertEquals($test->run('b', 'a', 'r'), 'second');
        $this->assertEquals($test->run('l', 'o', 'l'), 'third');
    }

我想这就是你要找的东西,但如果我有误解,请告诉我。

现在就模拟任何东西而言,您可以根据需要模拟多次,但您不会希望使用与设置中相同的名称来模拟它,否则每次使用它时您指的是设置。如果您需要在不同的场景中测试类似的方法,那么为每个测试模拟它。您可以在设置中创建一个模拟,但对于一个测试,在单个测试中使用类似项目的不同模拟,但不是全局名称。

【讨论】:

  • 我认为这应该是公认的答案,但还请注意,您用于 at() 的索引基于对该模拟对象的调用总数,而不是对该模拟对象的调用次数具体方法。因此,如果对同一对象上的其他方法进行其他调用,则需要相应地增加索引。
  • 问题是如何在指定的测试中改变模拟方法的行为。 at() 之类的计数器在这里不适用,因为每个测试都可以按任何顺序独立运行。并且在每个测试中都需要得到模拟方法的预期行为。我对深度嘲弄关系也有同样的问题。我暂时没有找到答案。
  • at() 在 PHPUnit 9 中已弃用,将在 PHPUnit 10 中删除。github.com/sebastianbergmann/phpunit/issues/4297
【解决方案2】:

您可以使用 lambda 回调来做到这一点:

$one_of_many_methods_return = true;
$my_mock->expects($this->any())
        ->method('one_of_many_methods')
        ->will(
             $this->returnCallback(
                 function () use (&$one_of_many_methods_return) {
                     return $one_of_many_methods_return;
                 }
              )         
          );
$this->assertTrue($my_mock->one_of_many_methods());

$one_of_many_methods_return = false;

$this->assertFalse($my_mock->one_of_many_methods());    

注意use 语句中的&

【讨论】:

  • 这对于一次性值很有用,尽管测试方法共享属性 ($this->one_of_many_methods_return) 比引用 (&$one_of_many_methods_return) 更容易。你也可以直接使用方法,比如$this->returnCallback([$this, 'someMethod'])。另请注意,PHP $this 和私有/受保护的属性/方法。
  • 巧妙的解决方案! :)
【解决方案3】:

一种方法是使用data provider,例如:

/**
 * @dataProvider methodValueProvider
 */
public function testMethodReturnValues($method, $expected) {
  // ...
  $my_mock->expects($this->any())
          ->method($method)
          ->will($this->returnValue($expected));
  // ...
}

public function methodValueProvider() {
  return array(
    array('one_of_many_methods', true),
    array('another_one_of_many', false)
  );
}

编辑:对不起,我误读了你的问题。以上(显然)不是您要查找的内容。

【讨论】:

    【解决方案4】:

    我没有尝试过,但你能不能在设置中设置模拟,然后在每个测试中:

    public function testMethodReturnsTrue
    {
        $this->my_mock->will($this->returnValue(true));
        $this->assertTrue( ... );
        ...
    }
    

    我不确定这是否可行,因为我试图在测试中设置 will() 方法,而不是在创建初始模拟时。

    【讨论】:

      【解决方案5】:

      与尝试覆盖模拟方法相比,我发现覆盖模拟对象本身更容易。例如:

      class ThingTest extends \PHPUnit_Framework_TestCase
          public function setUp()
          {
              $this->initFoo();
              $this->initBar();
          }
      
          public function testOne()
          {
              // Uses default [method => value] map for foo and bar
              $this->assertSomething($this->thing->someMethod());
          }
      
          public function testTwo()
          {
              // Override foo's map
              $this->initFoo(['method1' => 'some other value']);
              $this->assertSomethingElse($this->thing->someMethod());
          }
      
          public function testThree()
          {
              // Override bar explicitly, so we can use 'once'
              $this->initBar([]);
              $this->bar->expects($this->once())
                        ->method('method1');
              $this->thing->someOtherMethod();
          }
      
          private function initFoo($methods = null)
          {
              $this->init('foo',
                          $this->getMock('Foo'),
                          is_null($methods)? ['method1' => 'default value 1']
                                          : $methods);
          }
      
          private function initBar($methods = null)
          {
              $this->init('bar',
                          $this->getMock('Bar'),
                          is_null($methods)? ['method1' => 'default value 1']
                                           : $methods);
          }
      
          private function init($name, $object, $methods)
          {
              $this->$name = $object;
              foreach ($methods as $method => $value) {
                  $this->$name->expects($this->any())
                              ->method($method)
                              ->will($this->returnValue($value));
              }
              $this->thing = new Thing($this->foo, $this->bar);
          }
      }
      

      【讨论】:

        【解决方案6】:

        您也可以在单独的进程中运行测试:

        /**
         * @runTestsInSeparateProcesses b/c we change the return value of same expectation
         * @see http://stackoverflow.com/questions/13631855
         */
        class ThingTest extends \PHPUnit_Framework_TestCase
        {
            public function setUp() {
                $this->config = Mockery::mock('alias:Config');
            }
        
            public function test_thing_with_valid_config() {
                $this->config_set('default', 'valid');
                $sut = new \Thing();
            }
        
            /**
             * @dataProvider provides_broken_configs
             * @expectedException \RuntimeException
             */
            public function test_thing_with_broken_config($default) {
                $this->config_set('default', $default);
                $sut = new \Thing();
            }
        
            public function provides_broken_configs() {
                return [ [ null ] ];
            }
        
            protected function config_set($key, $value) {
                $this->config->shouldReceive('get')->with($key)->andReturn($value);
            }
        }
        

        在这个例子中,我碰巧使用了 Mockery,但模式是一样的。因为每个测试每次运行都有新的内存,所以我们不会遇到“覆盖”先前设置的期望的限制。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2017-01-28
          • 1970-01-01
          • 2016-01-05
          • 1970-01-01
          • 2015-10-27
          • 2014-07-06
          • 2019-01-19
          • 2015-04-04
          相关资源
          最近更新 更多