phpunit mock方法具有不同参数的多个调用

phpunit mock方法具有不同参数的多个调用,php,mocking,phpunit,Php,Mocking,Phpunit,有没有办法为不同的输入参数定义不同的模拟期望?例如,我有一个名为DB的数据库层类。此类具有名为“Query(string$Query)”的方法,该方法在输入时接受SQL查询字符串。我是否可以为此类(DB)创建模拟,并根据输入查询字符串为不同的查询方法调用设置不同的返回值?PHPUnit模拟库(默认情况下)仅根据传递给expects参数的匹配器和传递给方法的约束来确定期望是否匹配。因此,两个expect调用将失败,这两个调用只在传递给with的参数中存在差异,因为两者都将匹配,但只有一个将验证为具

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

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


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

例子:

重现两个->with()调用不起作用的原因: Mockery()似乎支持这一点。在我的例子中,我想检查是否在数据库上创建了两个索引:

嘲弄,作品:

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

mockry还有一个更好的语法IMHO。它似乎比PHPUnits内置的mocking功能慢了一点,但是YMMV。

如果可以避免,那么使用
at()
并不理想,因为

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

从4.1开始,您可以将
与连续的
一起使用

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

据我所知,解决这个问题的最佳方法是使用PHPUnit的值映射功能

示例来自:

此测试通过。如您所见:

  • 当使用参数“a”和“b”调用函数时,将返回“d”
  • 使用参数“e”和“f”调用函数时,返回“h”
据我所知,此功能是在PHPUnit 3.6中引入的,因此它已经足够“古老”,可以安全地用于几乎任何开发或登台环境以及任何持续集成工具。

Intro 好吧,我知道有一个解决方案是为嘲弄提供的,所以我不喜欢嘲弄,我会给你们一个预言的替代方案,但我建议你们首先去做

长话短说:“Prophecy使用一种称为消息绑定的方法——这意味着该方法的行为不会随着时间的推移而改变,而是被另一种方法改变。”

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

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

$chunk->processingInProgress();
$this->chunksStorage->updateChunk($chunk);
可以包装为:

$processorChunkStorage->persistChunkToInProgress($chunk);

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

谢谢你的帮助!你的回答完全解决了我的问题。另外,有时TDD开发对我来说很可怕,因为我必须为简单的体系结构使用如此大的解决方案:)这是一个很好的答案,真的帮助我理解了PHPUnit mocks。谢谢!!你也可以使用
$This->anythis()
作为
->logicalOr()
的参数之一,让你能够为你感兴趣的参数以外的其他参数提供默认值。我想知道没有人提到,使用“->logicalOr()”你不能保证(在这种情况下)这两个参数都被调用了。所以这并不能真正解决问题。除了下面的答案之外,你还可以使用这个答案中的方法:我喜欢这个答案,这是2016年的最佳答案。比公认的答案更好。如何为这两个不同的参数返回不同的内容?@emaillenin在类似的方法。仅供参考,我当时正在使用PHPUnit 4.0.20,并收到错误
致命错误:调用未定义的方法PHPUnit\u Framework\u MockObject\u Builder\u InvocationMocker::WithContinuous()
,在与Composer的快照中升级到4.1,并且它正在工作。
将返回执行调用
杀死它。
$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);
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'));
    }
}
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;
    }
}
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);
    }
}
$chunk->processingInProgress();
$this->chunksStorage->updateChunk($chunk);
$processorChunkStorage->persistChunkToInProgress($chunk);