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

gtlvzcf8  于 2023-03-07  发布在  PHP
关注(0)|答案(6)|浏览(252)

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

xam8gpfp

xam8gpfp1#

如果可以避免使用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);

PHPUnit 10删除了withConsecutive。您可以通过以下方式获得类似的功能:

$mock->expects($this->exactly(2))
    ->method('set')
    ->willReturnCallback(fn (string $property, int $value) => match (true) {
        $property === 'foo' && $value > 0,
        $property === 'bar' && $value > 0 => $mock->$property = $value,
        default => throw new LogicException()
    });

很明显,它更丑,也不太一样,但这就是现状。你可以在这里读到更多关于替代品的信息:https://github.com/sebastianbergmann/phpunit/issues/4026和此处:https://github.com/sebastianbergmann/phpunit/issues/4026#issuecomment-825453794

wwodge7n

wwodge7n2#

PHPUnit模拟库(默认情况下)仅根据传递给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
du7egjpx

du7egjpx3#

根据我的发现,解决这个问题的最佳方法是使用PHPUnit的值Map功能。
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中引入的,所以它已经足够“古老”了,几乎可以在任何开发或过渡环境中安全地使用,也可以与任何持续集成工具一起使用。

qxsslcnc

qxsslcnc4#

看起来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也有一个更好的语法IMHO。它似乎比PHPUnits内置的模拟功能慢一点,但是YMMV。

p5cysglq

p5cysglq5#

我们正在尝试在PHP8.1上用Phpunit 10升级测试,作为我们映像/库的年度升级。
在Phpunit 10上,at()和withConsecutive()已弃用。
@Radu Murzea的解决方案在大多数情况下有效:不是我们的!
我需要模拟MongoDB调用:参数有时是MongoDB\ObjectId;returnValueMap()使用===比较接收到的参数:如果对象比较失败,php文档显示php.net/manual/en/language.oop5.object-comparison.php
我模拟MongoDB FindOne的解决方案如下:

$map = [
        [
            ['_id' => new ObjectId("5825cfc1316f54c6128b4572"),],
            [],
            ['_id' => new ObjectId("5825cfc1316f54c6128b4572"), 'username' => 'test']
        ],
        [
            ['agencyIds' => new ObjectId("5825cfc1316f54c6128b4572"),],
            ['_id'],
            false
        ],
        [
            ['agencyIds' => new ObjectId("5825cfc1316f54c6128b4572"),],
            ['_id'],
            false
        ],
    ];

    $mongoDBUsersCollectionMock = $this->createMock(MongoDBCollection::class);
    $mongoDBUsersCollectionMock
        ->method('findOne')
        ->with($this->anything())
        ->will($this->returnCallback(
            function($filter, $options) use (&$map){
                list($mockedFilter, $mockedOptions, $mockedReturn) = array_shift($map);
                // if contains object remember don't use === because mean the exactly the same object
                // ref: https://www.php.net/manual/en/language.oop5.object-comparison.php
                if ($filter == $mockedFilter && $options == $mockedOptions){
                    return $mockedReturn;
                }
            }
        ));
a2mppw5e

a2mppw5e6#

简介

好的,我看到有一个解决方案提供了嘲笑,所以我不喜欢嘲笑,我会给你一个预言替代品,但我会建议你首先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开头;* * 因为你很少需要编写这样的单元测试,我认为这是一个很好的解决方案,而且它肯定很容易遵循,调试,因为它实际上描述了程序的执行。
顺便说一句:还有第二种选择,但需要修改我们正在测试的代码。我们可以 Package 麻烦制造者,并将他们移到一个单独的类中:

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

可以 Package 为:

$processorChunkStorage->persistChunkToInProgress($chunk);

就这样了,但我不想为它创建另一个类,我更喜欢第一个类.

相关问题