วิธีการจำลอง phpunit การโทรหลายครั้งพร้อมอาร์กิวเมนต์ที่แตกต่างกัน


117

มีวิธีใดในการกำหนดการจำลองที่แตกต่างกันสำหรับอาร์กิวเมนต์อินพุตที่แตกต่างกันหรือไม่? ตัวอย่างเช่นฉันมีคลาสเลเยอร์ฐานข้อมูลที่เรียกว่า DB คลาสนี้มีเมธอดที่เรียกว่า "Query (string $ query)" ซึ่งเมธอดนั้นใช้สตริงเคียวรี SQL ในอินพุต ฉันสามารถสร้างการจำลองสำหรับคลาสนี้ (DB) และตั้งค่าการส่งคืนที่แตกต่างกันสำหรับการเรียกวิธีการสอบถามที่แตกต่างกันซึ่งขึ้นอยู่กับสตริงแบบสอบถามอินพุตได้หรือไม่


นอกจากคำตอบด้านล่างแล้วคุณยังสามารถใช้วิธีการในคำตอบนี้stackoverflow.com/questions/5484602/…
Schleis

ฉันชอบคำตอบนี้stackoverflow.com/a/10964562/614709
yitznewton

คำตอบ:


132

ห้องสมุด 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

7
ขอบคุณสำหรับความช่วยเหลือของคุณ! คำตอบของคุณช่วยแก้ปัญหาของฉันได้อย่างสมบูรณ์ ปล. บางครั้งการพัฒนา TDD ก็ดูน่ากลัวสำหรับฉันเมื่อฉันต้องใช้โซลูชันขนาดใหญ่เช่นนี้สำหรับสถาปัตยกรรมที่เรียบง่าย :)
Aleksei Kornushkin

1
นี่เป็นคำตอบที่ดีช่วยให้ฉันเข้าใจ PHPUnit mocks ได้จริงๆ ขอบคุณ !!
Steve Bauman

คุณยังสามารถใช้$this->anything()เป็นหนึ่งในพารามิเตอร์เพื่อ->logicalOr()ให้คุณระบุค่าเริ่มต้นสำหรับอาร์กิวเมนต์อื่น ๆ นอกเหนือจากที่คุณสนใจได้อีกด้วย
MatsLindh

2
ฉันสงสัยว่าไม่มีใครพูดถึงด้วย "-> logicalOr ()" คุณจะไม่รับประกันว่า (ในกรณีนี้) ทั้งสองอาร์กิวเมนต์ถูกเรียก นี่จึงไม่สามารถแก้ปัญหาได้อย่างแท้จริง
user3790897

184

ไม่เหมาะอย่างยิ่งที่จะใช้at()หากคุณสามารถหลีกเลี่ยงได้เพราะตามที่เอกสารอ้าง

พารามิเตอร์ $ index สำหรับตัวจับคู่ at () หมายถึงดัชนีเริ่มต้นที่ศูนย์ในการเรียกใช้เมธอดทั้งหมดสำหรับอ็อบเจ็กต์จำลองที่กำหนด ใช้ความระมัดระวังเมื่อใช้ตัวจับคู่นี้เนื่องจากอาจนำไปสู่การทดสอบที่เปราะบางซึ่งเชื่อมโยงกับรายละเอียดการใช้งานที่เฉพาะเจาะจงมากเกินไป

ตั้งแต่ 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
คำตอบที่ดีที่สุด ณ ปี 2559 ดีกว่าคำตอบที่ยอมรับ
Matthew Housser

จะส่งคืนสิ่งที่แตกต่างกันสำหรับพารามิเตอร์ที่ต่างกันสองตัวนี้ได้อย่างไร
Lenin Raj Rajasekaran

@emaillenin โดยใช้ willReturnOnConsecutiveCalls ในลักษณะเดียวกัน
xarlymg89

FYI ฉันใช้ PHPUnit 4.0.20 และได้รับข้อผิดพลาดFatal error: Call to undefined method PHPUnit_Framework_MockObject_Builder_InvocationMocker::withConsecutive()อัปเกรดเป็น 4.1 ในพริบตาด้วย Composer และใช้งานได้
quickshiftin

ที่willReturnOnConsecutiveCallsฆ่ามัน
Rafael Barros

18

จากสิ่งที่ฉันพบวิธีที่ดีที่สุดในการแก้ปัญหานี้คือการใช้ฟังก์ชันแผนที่ค่าของ 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

Intro

โอเคฉันเห็นว่ามีทางออกหนึ่งสำหรับการล้อเลียนดังนั้นเนื่องจากฉันไม่ชอบการล้อเลียนฉันจะให้ทางเลือกในการพยากรณ์แก่คุณ แต่ฉันขอแนะนำให้คุณอ่านความแตกต่างระหว่างการเยาะเย้ยและคำทำนายก่อน

เรื่องสั้นขนาดยาว : "คำทำนายใช้วิธีการที่เรียกว่าการผูกข้อความ - หมายความว่าพฤติกรรมของวิธีการนี้ไม่เปลี่ยนแปลงเมื่อเวลาผ่านไป แต่จะเปลี่ยนไปโดยวิธีอื่น"

รหัสปัญหาในโลกแห่งความเป็นจริงที่จะครอบคลุม

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 solution

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

สรุป

อีกครั้งคำทำนายน่ากลัวกว่า! เคล็ดลับของฉันคือใช้ประโยชน์จากลักษณะการผูกการส่งข้อความของคำทำนายและแม้ว่ามันจะดูเหมือนรหัสนรกเรียกกลับจาวาสคริปต์แบบธรรมดาโดยเริ่มต้นด้วย$ self = $ this; เนื่องจากคุณแทบจะไม่ต้องเขียนการทดสอบหน่วยเช่นนี้ฉันคิดว่ามันเป็นทางออกที่ดีและง่ายต่อการติดตามแก้ไขข้อบกพร่องเนื่องจากอธิบายถึงการทำงานของโปรแกรม

BTW: มีทางเลือกที่สอง แต่ต้องเปลี่ยนรหัสที่เรากำลังทดสอบ เราสามารถห่อตัวผู้ก่อปัญหาและย้ายไปยังชั้นเรียนแยกกัน:

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

สามารถห่อเป็น:

$processorChunkStorage->persistChunkToInProgress($chunk);

และนั่นก็เป็นเช่นนั้น แต่เนื่องจากฉันไม่ต้องการสร้างคลาสอื่นฉันจึงชอบคลาสแรกมากกว่า

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.