phpunitモックメソッドの引数が異なる複数の呼び出し


117

異なる入力引数に対して異なるmock-expectを定義する方法はありますか?たとえば、DBというデータベースレイヤークラスがあります。このクラスには "Query(string $ query)"というメソッドがあり、そのメソッドは入力時にSQLクエリ文字列を受け取ります。このクラス(DB)のモックを作成し、入力クエリ文字列に依存する異なるクエリメソッド呼び出しに異なる戻り値を設定できますか?


以下の回答に加えて、あなたもこの答えにメソッドを使用することができます。stackoverflow.com/questions/5484602/...
Schleis

回答:


131

PHPUnitモッキングライブラリ(デフォルト)は、expectsパラメーターに渡されたマッチャーとに渡された制約のみに基づいて期待値が一致するかどうかを決定しますmethod。このため、expect渡された引数のみが異なる2つの呼び出しは、with両方が一致するため失敗しますが、1つだけが期待どおりの動作であると確認されます。実際の実施例後の再現事例をご覧ください。


問題については、使用する必要がある->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)


2つの-> 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

1
これはすばらしい答えです。PHPUnitモックを理解するのに本当に役立ちました。ありがとう!!
Steve Bauman、2016年

また、使用することができます$this->anything()するパラメータの1つとして->logicalOr()、あなたが興味を持っているもの以外の引数にデフォルト値を提供できるようにする。
MatsLindh

2
「-> logicalOr()」を使用すると、(この場合は)両方の引数が呼び出されたことを保証できないことに誰も言及しないのではないでしょうか。だからこれは本当に問題を解決しません。
user3790897 2017

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年現在のベストアンサー。承認されたアンサーより優れています。
Matthew Housser 2016年

これらの2つの異なるパラメーターに対して異なるものを返す方法は?
Lenin Raj Rajasekaran 16

同様の方法でwillReturnOnConsecutiveCallsを使用する@emaillenin。
xarlymg89

ちなみに、私はPHPUnit 4.0.20を使用していて、エラーを受け取りました。ComposerでFatal error: Call to undefined method PHPUnit_Framework_MockObject_Builder_InvocationMocker::withConsecutive()4.1に一気にアップグレードすると、問題なく動作します。
quickshiftin 2017年

それをwillReturnOnConsecutiveCalls殺した。
ラファエルバロス2017

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には、より良い構文のIMHOもあります。PHPUnitsの組み込みモック機能よりも少し遅いようですが、YMMVです。


0

はじめに

Mockeryには1つのソリューションが用意されているので、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 Prophecyソリューション

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;で始まります。このような単体テストを書く必要はめったにないので、私はそれが良い解決策だと思います。実際にプログラムの実行を説明しているので、追跡、デバッグは間違いなく簡単です。

ところで:2つ目の方法がありますが、テストするコードを変更する必要があります。トラブルメーカーをラップして、別のクラスに移動できます。

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

次のようにラップできます:

$processorChunkStorage->persistChunkToInProgress($chunk);

それだけですが、別のクラスを作成したくなかったので、最初のクラスを使用します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.