다른 입력 인수에 대해 다른 모의 기대치를 정의하는 방법이 있습니까? 예를 들어, DB라는 데이터베이스 계층 클래스가 있습니다. 이 클래스에는 "Query (string $ query)"라는 메서드가 있으며이 메서드는 입력시 SQL 쿼리 문자열을 사용합니다. 이 클래스 (DB)에 대한 모의를 만들고 입력 쿼리 문자열에 따라 다른 Query 메서드 호출에 대해 다른 반환 값을 설정할 수 있습니까?
다른 입력 인수에 대해 다른 모의 기대치를 정의하는 방법이 있습니까? 예를 들어, DB라는 데이터베이스 계층 클래스가 있습니다. 이 클래스에는 "Query (string $ query)"라는 메서드가 있으며이 메서드는 입력시 SQL 쿼리 문자열을 사용합니다. 이 클래스 (DB)에 대한 모의를 만들고 입력 쿼리 문자열에 따라 다른 Query 메서드 호출에 대해 다른 반환 값을 설정할 수 있습니까?
답변:
PHPUnit Mocking 라이브러리 (기본적으로)는 expects
매개 변수에 전달 된 매처 와에 전달 된 제약 조건 만을 기준으로 기대치가 일치하는지 여부를 결정합니다 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)
<?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()
매개 변수 중 하나로 사용 하여 ->logicalOr()
관심있는 인수가 아닌 다른 인수에 대한 기본값을 제공 할 수 있습니다.
그것은 사용하기 적합 아니라 at()
당신이 그것을 피할 수 있다면 때문에 자신의 문서 주장으로
at () 매처에 대한 $ index 매개 변수는 주어진 모의 객체에 대한 모든 메서드 호출에서 0부터 시작하는 인덱스를 참조합니다. 특정 구현 세부 사항과 너무 밀접하게 연결된 취약한 테스트로 이어질 수 있으므로이 매처를 사용할 때주의하십시오.
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()
했으며 Composer를 사용하여 즉시 4.1로 업그레이드되었으며 작동 중입니다.
willReturnOnConsecutiveCalls
그것을 죽였다.
내가 찾은 바에 따르면이 문제를 해결하는 가장 좋은 방법은 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'));
}
}
이 테스트를 통과했습니다. 보시다시피 :
내가 알 수 있듯이이 기능은 PHPUnit 3.6 에 도입 되었으므로 거의 모든 개발 또는 스테이징 환경과 모든 지속적인 통합 도구에서 안전하게 사용할 수있을만큼 "오래된"기능입니다.
Mockery ( https://github.com/padraic/mockery )가 이것을 지원하는 것 같습니다. 제 경우에는 데이터베이스에 2 개의 인덱스가 생성되었는지 확인하고 싶습니다.
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);
Mockery는 또한 더 좋은 구문 IMHO를 가지고 있습니다. PHPUnits 내장 조롱 기능보다 약간 느리지 만 YMMV입니다.
좋습니다. Mockery에 대해 제공되는 솔루션이 하나 있습니다. Mockery가 마음에 들지 않으므로 Prophecy 대안을 제공하겠습니다.하지만 먼저 Mockery와 Prophecy의 차이점에 대해 먼저 읽어 볼 것을 제안합니다 .
간단히 말해서 "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의 메시징 바인딩 특성을 활용하는 것이며, 슬프게도 $ self = $ this로 시작하는 전형적인 콜백 자바 스크립트 지옥 코드처럼 보이지만 ; 이와 같은 단위 테스트를 거의 작성하지 않아도되기 때문에 저는 이것이 좋은 솔루션이라고 생각하며 실제로 프로그램 실행을 설명하기 때문에 따르고 디버그하기 쉽습니다.
BTW : 두 번째 대안이 있지만 테스트중인 코드를 변경해야합니다. 우리는 문제를 일으키는 사람들을 감싸서 별도의 클래스로 옮길 수 있습니다.
$chunk->processingInProgress();
$this->chunksStorage->updateChunk($chunk);
다음과 같이 래핑 될 수 있습니다.
$processorChunkStorage->persistChunkToInProgress($chunk);
그게 다 였지만 다른 클래스를 만들고 싶지 않았기 때문에 첫 번째 클래스를 선호합니다.