metode tiruan phpunit beberapa panggilan dengan argumen berbeda

117

Apakah ada cara untuk mendefinisikan ekspektasi palsu yang berbeda untuk argumen input yang berbeda? Misalnya, saya memiliki kelas lapisan database yang disebut DB. Kelas ini memiliki metode yang disebut "Query (string $ query)", metode tersebut mengambil string kueri SQL pada input. Dapatkah saya membuat tiruan untuk kelas (DB) ini dan menetapkan nilai kembalian yang berbeda untuk panggilan metode Kueri yang berbeda yang bergantung pada string kueri masukan?

Aleksei Kornushkin
sumber
Selain jawaban di bawah ini, Anda juga dapat menggunakan metode dalam jawaban ini: stackoverflow.com/questions/5484602/…
Schleis
Saya suka jawaban ini stackoverflow.com/a/10964562/614709
yitznewton

Jawaban:

132

Library PHPUnit Mocking (secara default) menentukan apakah ekspektasi cocok hanya berdasarkan matcher yang diteruskan ke expectsparameter dan batasan yang diteruskan ke method. Karena itu, dua expectpanggilan yang hanya berbeda dalam argumen yang diteruskan withakan gagal karena keduanya akan cocok tetapi hanya satu yang akan memverifikasi sebagai memiliki perilaku yang diharapkan. Lihat kotak reproduksi setelah contoh kerja sebenarnya.


Untuk masalah Anda, Anda perlu menggunakan ->at()atau ->will($this->returnCallback(seperti yang dijelaskan dalam another question on the subject.

Contoh:

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

Mereproduksi:

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)


Reproduksi mengapa dua -> dengan () panggilan tidak berfungsi:

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

}

Hasil dalam

 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
edorian
sumber
7
terima kasih atas bantuan Anda! Jawaban Anda benar-benar memecahkan masalah saya. PS Kadang-kadang pengembangan TDD tampak menakutkan bagi saya ketika saya harus menggunakan solusi besar seperti itu untuk arsitektur sederhana :)
Aleksei Kornushkin
1
Ini adalah jawaban yang bagus, sangat membantu saya memahami tiruan PHPUnit. Terima kasih!!
Steve Bauman
Anda juga dapat menggunakan $this->anything()sebagai salah satu parameter untuk ->logicalOr()memungkinkan Anda memberikan nilai default untuk argumen lain selain yang Anda minati.
MatsLindh
2
Saya bertanya-tanya tidak ada yang menyebutkan, bahwa dengan "-> logicalOr ()" Anda tidak akan menjamin bahwa (dalam kasus ini) kedua argumen telah dipanggil. Jadi ini tidak benar-benar menyelesaikan masalah.
pengguna3790897
182

Ini tidak ideal untuk digunakan at()jika Anda dapat menghindarinya karena seperti yang diklaim oleh dokumen mereka

Parameter $ index untuk at () matcher mengacu pada indeks, dimulai dari nol, di semua pemanggilan metode untuk objek tiruan tertentu. Berhati-hatilah saat menggunakan matcher ini karena dapat menyebabkan pengujian rapuh yang terlalu terkait erat dengan detail implementasi tertentu.

Sejak 4.1 Anda dapat menggunakan withConsecutivemis.

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

Jika Anda ingin membuatnya kembali pada panggilan berturut-turut:

  $mock->method('set')
         ->withConsecutive([$argA1, $argA2], [$argB1], [$argC1, $argC2])
         ->willReturnOnConsecutiveCalls($retValueA, $retValueB, $retValueC);
hirowatari
sumber
22
Jawaban terbaik tahun 2016. Lebih baik dari jawaban yang diterima.
Matthew Housser
Bagaimana cara mengembalikan sesuatu yang berbeda untuk dua parameter berbeda tersebut?
Lenin Raj Rajasekaran
@emaillenin menggunakan willReturnOnConsecutiveCalls dengan cara yang sama.
xarlymg89
FYI, saya menggunakan PHPUnit 4.0.20 dan menerima kesalahan Fatal error: Call to undefined method PHPUnit_Framework_MockObject_Builder_InvocationMocker::withConsecutive(), ditingkatkan ke 4.1 dalam sekejap dengan Komposer dan itu berfungsi.
quickshiftin
The willReturnOnConsecutiveCallsmembunuhnya.
Rafael Barros
17

Dari apa yang saya temukan, cara terbaik untuk mengatasi masalah ini adalah dengan menggunakan fungsionalitas peta nilai PHPUnit.

Contoh dari dokumentasi 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'));
    }
}

Tes ini berhasil. Seperti yang Anda lihat:

  • ketika fungsi dipanggil dengan parameter "a" dan "b", "d" dikembalikan
  • ketika fungsi dipanggil dengan parameter "e" dan "f", "h" dikembalikan

Dari apa yang saya ketahui, fitur ini telah diperkenalkan di PHPUnit 3.6 , jadi fitur ini cukup "tua" sehingga dapat digunakan dengan aman di hampir semua lingkungan pengembangan atau pementasan dan dengan alat integrasi berkelanjutan apa pun.

Radu Murzea
sumber
6

Tampaknya Mockery ( https://github.com/padraic/mockery ) mendukung ini. Dalam kasus saya, saya ingin memeriksa bahwa 2 indeks dibuat pada database:

Ejekan, karya:

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, ini gagal:

$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 juga memiliki sintaks IMHO yang lebih bagus. Tampaknya sedikit lebih lambat dari kemampuan tiruan bawaan PHPUnits, tetapi YMMV.

joerx
sumber
0

Intro

Oke saya melihat ada satu solusi yang disediakan untuk Mockery, jadi karena saya tidak suka Mockery, saya akan memberi Anda alternatif Nubuat tetapi saya sarankan Anda terlebih dahulu membaca tentang perbedaan antara Mockery dan Prophecy terlebih dahulu.

Singkat cerita : "Nubuatan menggunakan pendekatan yang disebut pengikatan pesan - itu berarti bahwa perilaku metode tidak berubah seiring waktu, tetapi diubah oleh metode lain."

Kode bermasalah dunia nyata untuk dibahas

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

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

Ringkasan

Sekali lagi, Nubuat lebih dahsyat! Trik saya adalah memanfaatkan sifat pengikatan pesan dari Prophecy dan meskipun sayangnya itu terlihat seperti kode neraka javascript panggilan balik yang khas, dimulai dengan $ self = $ this; karena Anda sangat jarang harus menulis unit test seperti ini, saya pikir ini adalah solusi yang bagus dan pasti mudah diikuti, debug, karena ini benar-benar menggambarkan eksekusi program.

BTW: Ada alternatif kedua tetapi perlu mengubah kode yang kami uji. Kita bisa membungkus pembuat onar dan memindahkan mereka ke kelas terpisah:

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

bisa dibungkus sebagai:

$processorChunkStorage->persistChunkToInProgress($chunk);

dan hanya itu, tetapi karena saya tidak ingin membuat kelas lain untuk itu, saya lebih suka yang pertama.

Lukas Lukac
sumber