अलग तर्क के साथ phpunit नकली विधि कई कॉल


117

क्या अलग-अलग इनपुट तर्कों के लिए विभिन्न मॉक-एक्सपेक्ट्स को परिभाषित करने का कोई तरीका है? उदाहरण के लिए, मेरे पास डीबी नामक डेटाबेस लेयर क्लास है। इस वर्ग में "क्वेरी (स्ट्रिंग $ क्वेरी)" नामक विधि है, यह विधि इनपुट पर SQL क्वेरी स्ट्रिंग लेती है। क्या मैं इस वर्ग (DB) के लिए नकली बना सकता हूं और विभिन्न क्वेरी विधि कॉल के लिए अलग-अलग रिटर्न मान सेट कर सकता हूं जो इनपुट क्वेरी स्ट्रिंग पर निर्भर करता है?


नीचे दिए गए उत्तर के अलावा, आप इस उत्तर में विधि का भी उपयोग कर सकते हैं: stackoverflow.com/questions/5484602/…
Schleis

मुझे यह उत्तर पसंद है stackoverflow.com/a/10964562/614709
yitznewton

जवाबों:


131

PHPUnit मॉकिंग लाइब्रेरी (डिफ़ॉल्ट रूप से) यह निर्धारित करती है कि क्या एक अपेक्षा पूरी तरह से मैचर्स के आधार पर मेल खाती है जो 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";
    }
}

reproduces:

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
आपकी सहायताके लिए धन्यवाद! आपके जवाब से मेरी समस्या पूरी तरह हल हो गई। PS कभी-कभी TDD विकास मुझे भयानक लगता है जब मुझे सरल वास्तुकला के लिए ऐसे बड़े समाधानों का उपयोग करना पड़ता है :)
अलेक्सी कोर्नुशिन

1
यह एक महान जवाब है, वास्तव में मुझे PHPUnit मोक्स को समझने में मदद मिली है। धन्यवाद!!
स्टीव ब्यूमन

आप उन $this->anything()मापदंडों में से एक के रूप में भी उपयोग कर सकते हैं ->logicalOr(), जो आपको अपनी रुचि के अलावा अन्य तर्कों के लिए एक डिफ़ॉल्ट मान प्रदान करने की अनुमति देता है।
MatsLindh

2
कोई भी सोचता नहीं है, कि "-> तार्किकऑर ()" के साथ आप यह गारंटी नहीं देंगे कि (इस मामले में) दोनों तर्कों को बुलाया गया है। तो यह वास्तव में समस्या को हल नहीं करता है।
user3790897

182

यह उपयोग करने के लिए आदर्श नहीं है 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
2016 के रूप में सर्वश्रेष्ठ उत्तर। स्वीकृत उत्तर से बेहतर।
मैथ्यू हाउससर

उन दो अलग-अलग मापदंडों के लिए कुछ अलग कैसे लौटाएं?
लेनिन राज राजसेकरन

@emaillenin इसी तरह से willReturnOnConsistentCalls का उपयोग कर रहा है।
xarlymg89

FYI करें, मैं PHPUnit 4.0.20 का उपयोग कर रहा था और त्रुटि प्राप्त कर रहा था Fatal error: Call to undefined method PHPUnit_Framework_MockObject_Builder_InvocationMocker::withConsecutive(), संगीतकार के साथ एक तस्वीर में 4.1 में अपग्रेड किया गया और यह काम कर रहा है।
क्विकशिफ्टिन

willReturnOnConsecutiveCallsयह मार डाला।
राफेल बैरोस

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" वापस आ जाता है
  • जब फ़ंक्शन को "ई" और "एफ" के मापदंडों के साथ बुलाया जाता है, तो "एच" वापस आ जाता है

जो मैं बता सकता हूं, उससे यह सुविधा PHPUnit 3.6 में पेश की गई थी , इसलिए यह "पुरानी" पर्याप्त है कि इसे सुरक्षित रूप से किसी भी विकास या मंचन के वातावरण और किसी भी निरंतर एकीकरण उपकरण के साथ उपयोग किया जा सकता है।


6

ऐसा लगता है कि मॉकरी ( 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);

मॉकरी में एक अच्छे सिंटैक्स IMHO भी हैं। यह PHPUnits द्वारा निर्मित मज़ाक क्षमता की तुलना में एक कम धीमी प्रतीत होता है, लेकिन YMMV।


0

पहचान

ठीक है, मैं देखता हूं कि मॉकरी के लिए एक समाधान प्रदान किया गया है, इसलिए जैसा कि मुझे मॉकरी पसंद नहीं है, मैं आपको एक भविष्यवाणी विकल्प देने जा रहा हूं, लेकिन मैं आपको पहले सुझाव दूंगा कि आप मॉकरी और भविष्यवाणी के बीच के अंतर के बारे में पढ़ें।

लंबी कहानी छोटी : "भविष्यवाणी संदेश बंधन नामक दृष्टिकोण का उपयोग करती है - इसका मतलब है कि विधि का व्यवहार समय के साथ नहीं बदलता है, बल्कि दूसरी विधि द्वारा बदल दिया जाता है।"

वास्तविक विश्व को कवर करने के लिए समस्याग्रस्त कोड

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 भविष्यवाणी समाधान

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

सारांश

एक बार फिर, भविष्यवाणी अधिक भयानक है! मेरी चाल भविष्यवाणी के संदेशवाहक बंधन प्रकृति का लाभ उठाने के लिए है और भले ही यह दुख की बात है कि यह एक विशिष्ट, कॉलबैक जावास्क्रिप्ट नरक कोड की तरह है, जिसकी शुरुआत $ स्वयं = $ से होती है; जैसा कि आप शायद ही कभी इकाई परीक्षणों को इस तरह लिखते हैं, मुझे लगता है कि यह एक अच्छा समाधान है और यह निश्चित रूप से पालन करना आसान है, डीबग करना, क्योंकि यह वास्तव में कार्यक्रम के निष्पादन का वर्णन करता है।

BTW: एक दूसरा विकल्प है, लेकिन हमारे द्वारा परीक्षण किए जा रहे कोड को बदलने की आवश्यकता है। हम संकटमोचन को लपेट कर एक अलग वर्ग में ले जा सकते हैं:

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

के रूप में लिपटे जा सकता है:

$processorChunkStorage->persistChunkToInProgress($chunk);

और ऐसा ही है, लेकिन जैसा कि मैं इसके लिए एक और वर्ग बनाना नहीं चाहता था, मैं पहले वाले को पसंद करता हूं।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.