phpunit 모의 메소드 다른 인수로 여러 호출


117

다른 입력 인수에 대해 다른 모의 기대치를 정의하는 방법이 있습니까? 예를 들어, DB라는 데이터베이스 계층 클래스가 있습니다. 이 클래스에는 "Query (string $ query)"라는 메서드가 있으며이 메서드는 입력시 SQL 쿼리 문자열을 사용합니다. 이 클래스 (DB)에 대한 모의를 만들고 입력 쿼리 문자열에 따라 다른 Query 메서드 호출에 대해 다른 반환 값을 설정할 수 있습니까?


아래 답변
외에도이

답변:


131

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)


두 개의-> 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
당신의 도움을 주셔서 감사합니다! 당신의 대답은 내 문제를 완전히 해결했습니다. 추신 : 때때로 TDD 개발은 간단한 아키텍처를 위해 큰 솔루션을 사용해야 할 때 저에게 끔찍한 것 같습니다. :)
Aleksei Kornushkin

1
이것은 훌륭한 대답이며 PHPUnit 모의를 이해하는 데 정말 도움이되었습니다. 감사!!
Steve Bauman 2016 년

또한 $this->anything()매개 변수 중 하나로 사용 하여 ->logicalOr()관심있는 인수가 아닌 다른 인수에 대한 기본값을 제공 할 수 있습니다.
MatsLindh

2
아무도 언급하지 않았는지 궁금합니다. "-> logicalOr ()"를 사용하면 (이 경우) 두 인수가 모두 호출되었음을 보장 할 수 없습니다. 따라서 이것은 실제로 문제를 해결하지 못합니다.
user3790897 jul.

182

그것은 사용하기 적합 아니라 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);

22
2016 년 베스트 답변. 수락 된 답변보다 낫습니다.
마태 복음 Housser

이 두 가지 다른 매개 변수에 대해 다른 것을 반환하는 방법은 무엇입니까?
레닌 주권 Rajasekaran

유사한 방식으로 willReturnOnConsecutiveCalls를 사용하는 @emaillenin.
xarlymg89

참고로, 저는 PHPUnit 4.0.20을 사용하고 있었고 오류가 발생 Fatal error: Call to undefined method PHPUnit_Framework_MockObject_Builder_InvocationMocker::withConsecutive()했으며 Composer를 사용하여 즉시 4.1로 업그레이드되었으며 작동 중입니다.
quickshiftin

willReturnOnConsecutiveCalls그것을 죽였다.
Rafael Barros

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 개의 인덱스가 생성되었는지 확인하고 싶습니다.

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입니다.


0

소개

좋습니다. 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;
    }
}

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의 메시징 바인딩 특성을 활용하는 것이며, 슬프게도 $ self = $ this로 시작하는 전형적인 콜백 자바 스크립트 지옥 코드처럼 보이지만 ; 이와 같은 단위 테스트를 거의 작성하지 않아도되기 때문에 저는 이것이 좋은 솔루션이라고 생각하며 실제로 프로그램 실행을 설명하기 때문에 따르고 디버그하기 쉽습니다.

BTW : 두 번째 대안이 있지만 테스트중인 코드를 변경해야합니다. 우리는 문제를 일으키는 사람들을 감싸서 별도의 클래스로 옮길 수 있습니다.

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

다음과 같이 래핑 될 수 있습니다.

$processorChunkStorage->persistChunkToInProgress($chunk);

그게 다 였지만 다른 클래스를 만들고 싶지 않았기 때문에 첫 번째 클래스를 선호합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.