phpunit模拟方法使用不同参数多次调用


117

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


除了下面的答案,你也可以使用这个答案的方法:stackoverflow.com/questions/5484602/...
Schleis

Answers:


131

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


对于您的问题,您需要使用->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

7
谢谢你的帮助!您的回答完全解决了我的问题。PS有时,当我不得不为简单的体系结构使用如此大的解决方案时,TDD开发对我来说似乎很可怕:)
Aleksei Kornushkin 2011年

1
这是一个很好的答案,确实帮助我了解了PHPUnit模拟。谢谢!!
史蒂夫·鲍曼

您还可以将其$this->anything()用作参数之一,->logicalOr()以便为您感兴趣的其他参数提供默认值
。– MatsLindh

2
我想知道没有人提到,使用“-> logicalOr()”不会保证(在这种情况下)两个参数都已被调用。因此,这并不能真正解决问题。
user3790897

182

at()如果可以避免使用,则不理想,因为正如他们的文档所声称的那样

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

22
截至2016年的最佳答案。优于公认的答案。
马修·侯塞尔

如何为这两个不同的参数返回不同的东西?
列宁·拉杰·拉贾塞卡兰(Linin Raj Rajasekaran),2016年

@emaillenin以类似的方式使用willReturnOnConsecutiveCalls。
xarlymg89 '16

仅供参考,我使用的是PHPUnit 4.0.20并收到错误消息Fatal error: Call to undefined method PHPUnit_Framework_MockObject_Builder_InvocationMocker::withConsecutive(),使用Composer很快将其升级到4.1,并且可以正常工作。
quickshiftin

willReturnOnConsecutiveCalls把它打死了。
拉斐尔·巴罗斯

17

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

来自PHPUnit文档的示例:

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中引入的,因此它已经“过时”,可以在几乎所有开发或登台环境以及任何持续集成工具中安全使用。


6

似乎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慢。


0

介绍

好的,我看到有一个针对Mockery的解决方案,因此,由于我不喜欢Mockery,我将为您提供一个Prophecy替代方案,但我建议您首先阅读Mockery和Prophecy之间的区别。

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

实际问题代码覆盖

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.