Phương thức giả lập phpunit nhiều cuộc gọi với các đối số khác nhau


117

Có cách nào để xác định các giả định khác nhau cho các đối số đầu vào khác nhau không? Ví dụ, tôi có lớp lớp cơ sở dữ liệu gọi là DB. Lớp này có phương thức gọi là "Truy vấn (chuỗi $ truy vấn)", phương thức đó lấy chuỗi truy vấn SQL trên đầu vào. Tôi có thể tạo giả cho lớp này (DB) và đặt các giá trị trả về khác nhau cho các lệnh gọi phương thức Truy vấn khác nhau phụ thuộc vào chuỗi truy vấn đầu vào không?


Ngoài câu trả lời dưới đây, bạn cũng có thể sử dụng phương pháp trong câu trả lời này: stackoverflow.com/questions/5484602/
mẹo

Tôi thích câu trả lời này stackoverflow.com/a/10964562/614709
yitznewton

Câu trả lời:


131

Thư viện Mocking của PHPUnit (theo mặc định) xác định liệu một kỳ vọng có khớp chỉ dựa trên trình so khớp được truyền cho expectstham số và ràng buộc được truyền tới hay không method. Bởi vì điều này, hai expectcuộc gọi chỉ khác nhau trong các đối số được chuyển đến withsẽ thất bại vì cả hai sẽ khớp nhưng chỉ một cuộc gọi sẽ xác minh là có hành vi dự kiến. Xem trường hợp sinh sản sau ví dụ làm việc thực tế.


Đối với bạn vấn đề bạn cần sử dụng ->at()hoặc ->will($this->returnCallback(như được nêu trong another question on the subject.

Thí dụ:

<?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";
    }
}

Sinh sản:

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)


Tái tạo lý do tại sao hai cuộc gọi -> với () không hoạt động:

<?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"));
    }

}

Kết quả trong

 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
Cảm ơn bạn đã giúp đỡ! Câu trả lời của bạn đã giải quyết hoàn toàn vấn đề của tôi. PS Đôi khi sự phát triển TDD có vẻ đáng sợ đối với tôi khi tôi phải sử dụng các giải pháp lớn như vậy cho kiến ​​trúc đơn giản :)
Aleksei Kornushkin

1
Đây là một câu trả lời tuyệt vời, thực sự đã giúp tôi hiểu được PHPUnit giả. Cảm ơn!!
Steve Bauman

Bạn cũng có thể sử dụng $this->anything()như một trong các tham số để ->logicalOr()cho phép bạn cung cấp giá trị mặc định cho các đối số khác ngoài tham số mà bạn quan tâm.
MatsLindh

2
Tôi tự hỏi không ai đề cập rằng, với "-> logicOr ()" bạn sẽ không đảm bảo rằng (trong trường hợp này) cả hai đối số đã được gọi. Vì vậy, điều này không thực sự giải quyết vấn đề.
dùng3790897

182

Nó không lý tưởng để sử dụng at()nếu bạn có thể tránh nó bởi vì như tài liệu của họ yêu cầu

Tham số $ index cho trình so khớp at () đề cập đến chỉ mục, bắt đầu từ 0, trong tất cả các yêu cầu phương thức cho một đối tượng giả định đã cho. Thận trọng khi sử dụng công cụ đối sánh này vì nó có thể dẫn đến các bài kiểm tra giòn, quá chặt chẽ với các chi tiết thực hiện cụ thể.

Kể từ 4.1 bạn có thể sử dụng, withConsecutivevd.

$mock->expects($this->exactly(2))
     ->method('set')
     ->withConsecutive(
         [$this->equalTo('foo'), $this->greaterThan(0)],
         [$this->equalTo('bar'), $this->greaterThan(0)]
       );

Nếu bạn muốn làm cho nó trở lại trong các cuộc gọi liên tiếp:

  $mock->method('set')
         ->withConsecutive([$argA1, $argA2], [$argB1], [$argC1, $argC2])
         ->willReturnOnConsecutiveCalls($retValueA, $retValueB, $retValueC);

22
Câu trả lời hay nhất năm 2016. Tốt hơn câu trả lời được chấp nhận.
Matthew Housser 18/03/2016

Làm thế nào để trả về một cái gì đó khác nhau cho hai tham số khác nhau?
Lenin Raj Rajasekaran

@emaillenin sử dụng willReturnOnConsceedCalls theo cách tương tự.
xarlymg89

FYI, tôi đã sử dụng PHPUnit 4.0.20 và nhận được lỗi Fatal error: Call to undefined method PHPUnit_Framework_MockObject_Builder_InvocationMocker::withConsecutive(), đã nâng cấp lên 4.1 trong tích tắc với Trình soạn thảo và nó đang hoạt động.
quickshiftin

Kẻ willReturnOnConsecutiveCallsđã giết nó.
Rafael Barros

17

Từ những gì tôi đã tìm thấy, cách tốt nhất để giải quyết vấn đề này là sử dụng chức năng bản đồ giá trị của PHPUnit.

Ví dụ từ tài liệu của 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'));
    }
}

Bài kiểm tra này đã qua. Bạn có thể thấy:

  • khi hàm được gọi với tham số "a" và "b", "d" được trả về
  • khi hàm được gọi với tham số "e" và "f", "h" được trả về

Từ những gì tôi có thể nói, tính năng này đã được giới thiệu trong PHPUnit 3.6 , do đó, nó "cũ" đến mức nó có thể được sử dụng an toàn trên hầu hết mọi môi trường phát triển hoặc dàn dựng và với bất kỳ công cụ tích hợp liên tục nào.


6

Có vẻ như Mockery ( https://github.com/padraic/mockery ) hỗ trợ điều này. Trong trường hợp của tôi, tôi muốn kiểm tra xem 2 chỉ mục được tạo trên cơ sở dữ liệu:

Mockery, tác phẩm:

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, điều này không thành công:

$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 cũng có một cú pháp IMHO đẹp hơn. Nó dường như chậm hơn một chút so với khả năng chế tạo tích hợp sẵn của PHPUnits, nhưng YMMV.


0

Giới thiệu

Được rồi tôi thấy có một giải pháp được cung cấp cho Mockery, vì vậy tôi không thích Mockery, tôi sẽ cung cấp cho bạn một giải pháp thay thế Tiên tri nhưng trước tiên tôi sẽ đề nghị bạn đọc về sự khác biệt giữa Mockery và Tiên tri.

Câu chuyện dài ngắn : "Lời tiên tri sử dụng cách tiếp cận được gọi là ràng buộc thông điệp - điều đó có nghĩa là hành vi của phương thức không thay đổi theo thời gian, mà thay đổi theo phương thức khác."

Mã có vấn đề trong thế giới thực

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;
    }
}

Giải pháp tiên tri 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);
    }
}

Tóm lược

Một lần nữa, lời tiên tri tuyệt vời hơn! Thủ thuật của tôi là tận dụng tính chất ràng buộc nhắn tin của Tiên tri và mặc dù đáng buồn là nó giống như một mã địa ngục javascript thông thường, gọi lại, bắt đầu bằng $ self = $ this; vì bạn rất hiếm khi phải viết các bài kiểm tra đơn vị như thế này Tôi nghĩ rằng đó là một giải pháp tốt và nó rất dễ thực hiện, gỡ lỗi, vì nó thực sự mô tả việc thực hiện chương trình.

BTW: Có một sự thay thế thứ hai nhưng yêu cầu thay đổi mã chúng tôi đang thử nghiệm. Chúng tôi có thể bao bọc những kẻ gây rối và chuyển chúng sang một lớp riêng:

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

có thể được bọc như:

$processorChunkStorage->persistChunkToInProgress($chunk);

và đó là nó nhưng vì tôi không muốn tạo một lớp khác cho nó, tôi thích lớp đầu tiên.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.