【问题标题】:phpunit mock method multiple calls with different argumentsphpunit mock 方法多次调用不同参数
【发布时间】:2011-08-24 17:19:09
【问题描述】:

有没有办法为不同的输入参数定义不同的模拟期望?例如,我有一个名为 DB 的数据库层类。此类具有称为“Query (string $query)”的方法,该方法在输入时采用 SQL 查询字符串。我可以为这个类(DB)创建模拟并为取决于输入查询字符串的不同查询方法调用设置不同的返回值吗?

【问题讨论】:

标签: php mocking phpunit


【解决方案1】:

如果可以避免使用at(),则不理想,因为as their docs claim

at() 匹配器的 $index 参数是指索引,从零开始,在给定模拟对象的所有方法调用中。使用此匹配器时要小心,因为它可能会导致与特定实现细节过于紧密相关的脆弱测试。

从 4.1 开始,您可以使用 withConsecutive 例如。

$mock->expects($this->exactly(2))
     ->method('set')
     ->withConsecutive(
         [$this->equalTo('foo'), $this->greaterThan(0)],
         [$this->equalTo('bar'), $this->greaterThan(0)]
       );

如果你想让它在连续调用时返回:

  $mock->method('set')
         ->withConsecutive([$argA1, $argA2], [$argB1], [$argC1, $argC2])
         ->willReturnOnConsecutiveCalls($retValueA, $retValueB, $retValueC);

【讨论】:

  • 截至 2016 年的最佳答案。优于接受的答案。
  • 如何为这两个不同的参数返回不同的东西?
  • @emaillenin 以类似的方式使用 willReturnOnConsecutiveCalls。
  • 仅供参考,我使用的是 PHPUnit 4.0.20 并收到错误 Fatal error: Call to undefined method PHPUnit_Framework_MockObject_Builder_InvocationMocker::withConsecutive(),使用 Composer 升级到 4.1 并且它正在工作。
  • willReturnOnConsecutiveCalls 杀死了它。
【解决方案2】:

PHPUnit Mocking 库(默认情况下)仅根据传递给expects 参数的匹配器和传递给method 的约束来确定期望是否匹配。因此,两个仅在传递给with 的参数上不同的expect 调用将失败,因为两者都会匹配,但只有一个会验证是否具有预期的行为。见实际工作示例后的复现案例。


对于您的问题,您需要使用->at()->will($this->returnCallback(,如another question on the subject 中所述。

示例:

<?php

class DB {
    public function Query($sSql) {
        return "";
    }
}

class fooTest extends PHPUnit_Framework_TestCase {


    public function testMock() {

        $mock = $this->getMock('DB', array('Query'));

        $mock
            ->expects($this->exactly(2))
            ->method('Query')
            ->with($this->logicalOr(
                 $this->equalTo('select * from roles'),
                 $this->equalTo('select * from users')
             ))
            ->will($this->returnCallback(array($this, 'myCallback')));

        var_dump($mock->Query("select * from users"));
        var_dump($mock->Query("select * from roles"));
    }

    public function myCallback($foo) {
        return "Called back: $foo";
    }
}

转载:

phpunit foo.php
PHPUnit 3.5.13 by Sebastian Bergmann.

string(32) "Called back: select * from users"
string(32) "Called back: select * from roles"
.

Time: 0 seconds, Memory: 4.25Mb

OK (1 test, 1 assertion)


重现为什么两个 ->with() 调用不起作用:

<?php

class DB {
    public function Query($sSql) {
        return "";
    }
}

class fooTest extends PHPUnit_Framework_TestCase {


    public function testMock() {

        $mock = $this->getMock('DB', array('Query'));
        $mock
            ->expects($this->once())
            ->method('Query')
            ->with($this->equalTo('select * from users'))
            ->will($this->returnValue(array('fred', 'wilma', 'barney')));

        $mock
            ->expects($this->once())
            ->method('Query')
            ->with($this->equalTo('select * from roles'))
            ->will($this->returnValue(array('admin', 'user')));

        var_dump($mock->Query("select * from users"));
        var_dump($mock->Query("select * from roles"));
    }

}

结果

 phpunit foo.php
PHPUnit 3.5.13 by Sebastian Bergmann.

F

Time: 0 seconds, Memory: 4.25Mb

There was 1 failure:

1) fooTest::testMock
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-select * from roles
+select * from users

/home/.../foo.php:27

FAILURES!
Tests: 1, Assertions: 0, Failures: 1

【讨论】:

  • 感谢您的帮助!你的回答完全解决了我的问题。附:当我不得不为简单的架构使用如此庞大的解决方案时,有时 TDD 开发对我来说似乎很可怕 :)
  • 这是一个很好的答案,真的帮助我理解 PHPUnit 模拟。谢谢!!
  • 您还可以使用$this-&gt;anything() 作为-&gt;logicalOr() 的参数之一,以便为您感兴趣的参数以外的其他参数提供默认值。
  • 我想知道没有人提到,使用“->logicalOr()”你不能保证(在这种情况下)两个参数都被调用了。所以这并不能真正解决问题。
【解决方案3】:

根据我的发现,解决此问题的最佳方法是使用 PHPUnit 的值映射功能。

来自PHPUnit's documentation的示例:

class SomeClass {
    public function doSomething() {}   
}

class StubTest extends \PHPUnit_Framework_TestCase {
    public function testReturnValueMapStub() {

        $mock = $this->getMock('SomeClass');

        // Create a map of arguments to return values.
        $map = array(
          array('a', 'b', 'd'),
          array('e', 'f', 'h')
        );  

        // Configure the mock.
        $mock->expects($this->any())
             ->method('doSomething')
             ->will($this->returnValueMap($map));

        // $mock->doSomething() returns different values depending on
        // the provided arguments.
        $this->assertEquals('d', $stub->doSomething('a', 'b'));
        $this->assertEquals('h', $stub->doSomething('e', 'f'));
    }
}

此测试通过。如您所见:

  • 使用参数“a”和“b”调用函数时,返回“d”
  • 当使用参数“e”和“f”调用函数时,返回“h”

据我所知,这个功能是在 PHPUnit 3.6 中引入的,所以它已经足够“老”了,可以安全地用于几乎任何开发或暂存环境以及任何持续集成工具.

【讨论】:

    【解决方案4】:

    Mockery (https://github.com/padraic/mockery) 似乎支持这一点。就我而言,我想检查是否在数据库上创建了 2 个索引:

    嘲讽,作品:

    use Mockery as m;
    
    //...
    
    $coll = m::mock(MongoCollection::class);
    $db = m::mock(MongoDB::class);
    
    $db->shouldReceive('selectCollection')->withAnyArgs()->times(1)->andReturn($coll);
    $coll->shouldReceive('createIndex')->times(1)->with(['foo' => true]);
    $coll->shouldReceive('createIndex')->times(1)->with(['bar' => true], ['unique' => true]);
    
    new MyCollection($db);
    

    PHPUnit,这失败了:

    $coll = $this->getMockBuilder(MongoCollection::class)->disableOriginalConstructor()->getMock();
    $db  = $this->getMockBuilder(MongoDB::class)->disableOriginalConstructor()->getMock();
    
    $db->expects($this->once())->method('selectCollection')->with($this->anything())->willReturn($coll);
    $coll->expects($this->atLeastOnce())->method('createIndex')->with(['foo' => true]);
    $coll->expects($this->atLeastOnce())->method('createIndex')->with(['bar' => true], ['unique' => true]);
    
    new MyCollection($db);
    

    Mockery 还有一个更好的语法恕我直言。它似乎比 PHPUnits 内置的模拟功能慢一点,但是 YMMV。

    【讨论】:

      【解决方案5】:

      简介

      好的,我看到为 Mockery 提供了一种解决方案,所以我不喜欢 Mockery,我会给你一个 Prophecy 替代方案,但我建议你先到 read about the difference between Mockery and Prophecy first.

      长话短说:“预言使用称为消息绑定的方法 - 这意味着该方法的行为不会随着时间而改变,而是会被其他方法改变。”

      要涵盖的真实世界有问题的代码

      class Processor
      {
          /**
           * @var MutatorResolver
           */
          private $mutatorResolver;
      
          /**
           * @var ChunksStorage
           */
          private $chunksStorage;
      
          /**
           * @param MutatorResolver $mutatorResolver
           * @param ChunksStorage   $chunksStorage
           */
          public function __construct(MutatorResolver $mutatorResolver, ChunksStorage $chunksStorage)
          {
              $this->mutatorResolver = $mutatorResolver;
              $this->chunksStorage   = $chunksStorage;
          }
      
          /**
           * @param Chunk $chunk
           *
           * @return bool
           */
          public function process(Chunk $chunk): bool
          {
              $mutator = $this->mutatorResolver->resolve($chunk);
      
              try {
                  $chunk->processingInProgress();
                  $this->chunksStorage->updateChunk($chunk);
      
                  $mutator->mutate($chunk);
      
                  $chunk->processingAccepted();
                  $this->chunksStorage->updateChunk($chunk);
              }
              catch (UnableToMutateChunkException $exception) {
                  $chunk->processingRejected();
                  $this->chunksStorage->updateChunk($chunk);
      
                  // Log the exception, maybe together with Chunk insert them into PostProcessing Queue
              }
      
              return false;
          }
      }
      

      PhpUnit预言解决方案

      class ProcessorTest extends ChunkTestCase
      {
          /**
           * @var Processor
           */
          private $processor;
      
          /**
           * @var MutatorResolver|ObjectProphecy
           */
          private $mutatorResolverProphecy;
      
          /**
           * @var ChunksStorage|ObjectProphecy
           */
          private $chunkStorage;
      
          public function setUp()
          {
              $this->mutatorResolverProphecy = $this->prophesize(MutatorResolver::class);
              $this->chunkStorage            = $this->prophesize(ChunksStorage::class);
      
              $this->processor = new Processor(
                  $this->mutatorResolverProphecy->reveal(),
                  $this->chunkStorage->reveal()
              );
          }
      
          public function testProcessShouldPersistChunkInCorrectStatusBeforeAndAfterTheMutateOperation()
          {
              $self = $this;
      
              // Chunk is always passed with ACK_BY_QUEUE status to process()
              $chunk = $this->createChunk();
              $chunk->ackByQueue();
      
              $campaignMutatorMock = $self->prophesize(CampaignMutator::class);
              $campaignMutatorMock
                  ->mutate($chunk)
                  ->shouldBeCalled();
      
              $this->mutatorResolverProphecy
                  ->resolve($chunk)
                  ->shouldBeCalled()
                  ->willReturn($campaignMutatorMock->reveal());
      
              $this->chunkStorage
                  ->updateChunk($chunk)
                  ->shouldBeCalled()
                  ->will(
                      function($args) use ($self) {
                          $chunk = $args[0];
                          $self->assertTrue($chunk->status() === Chunk::STATUS_PROCESSING_IN_PROGRESS);
      
                          $self->chunkStorage
                              ->updateChunk($chunk)
                              ->shouldBeCalled()
                              ->will(
                                  function($args) use ($self) {
                                      $chunk = $args[0];
                                      $self->assertTrue($chunk->status() === Chunk::STATUS_PROCESSING_UPLOAD_ACCEPTED);
      
                                      return true;
                                  }
                              );
      
                          return true;
                      }
                  );
      
              $this->processor->process($chunk);
          }
      }
      

      总结

      再一次,预言更棒了!我的诀窍是利用 Prophecy 的消息绑定特性,尽管它看起来很像典型的回调 javascript 地狱代码,以 $self = $this; 开头,因为您很少需要编写单元测试像这样,我认为这是一个很好的解决方案,而且它绝对易于理解、调试,因为它实际上描述了程序的执行。

      顺便说一句:还有第二种选择,但需要更改我们正在测试的代码。我们可以包装麻烦制造者并将它们移到单独的类中:

      $chunk->processingInProgress();
      $this->chunksStorage->updateChunk($chunk);
      

      可以包装成:

      $processorChunkStorage->persistChunkToInProgress($chunk);
      

      就是这样,但由于我不想为它创建另一个类,我更喜欢第一个。

      【讨论】:

        猜你喜欢
        • 2015-10-27
        • 1970-01-01
        • 2017-01-28
        • 2020-03-01
        • 2014-07-06
        • 2012-08-07
        • 2020-09-01
        • 1970-01-01
        • 2021-11-01
        相关资源
        最近更新 更多