異なる入力引数に対して異なるmock-expectを定義する方法はありますか?たとえば、DBというデータベースレイヤークラスがあります。このクラスには "Query(string $ query)"というメソッドがあり、そのメソッドは入力時にSQLクエリ文字列を受け取ります。このクラス(DB)のモックを作成し、入力クエリ文字列に依存する異なるクエリメソッド呼び出しに異なる戻り値を設定できますか?
異なる入力引数に対して異なるmock-expectを定義する方法はありますか?たとえば、DBというデータベースレイヤークラスがあります。このクラスには "Query(string $ query)"というメソッドがあり、そのメソッドは入力時にSQLクエリ文字列を受け取ります。このクラス(DB)のモックを作成し、入力クエリ文字列に依存する異なるクエリメソッド呼び出しに異なる戻り値を設定できますか?
回答:
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)
<?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
$this->anything()
するパラメータの1つとして->logicalOr()
、あなたが興味を持っているもの以外の引数にデフォルト値を提供できるようにする。
これは、使用に理想的ではありません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);
Fatal error: Call to undefined method PHPUnit_Framework_MockObject_Builder_InvocationMocker::withConsecutive()
4.1に一気にアップグレードすると、問題なく動作します。
willReturnOnConsecutiveCalls
殺した。
私が見つけたことから、この問題を解決する最良の方法は、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'));
}
}
このテストは合格です。ご覧のように:
私の知る限りでは、この機能はPHPUnit 3.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です。
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;
}
}
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);
それだけですが、別のクラスを作成したくなかったので、最初のクラスを使用します。