phpunit mock yöntemi farklı argümanlarla çoklu çağrılar


117

Farklı girdi argümanları için farklı model beklentileri tanımlamanın herhangi bir yolu var mı? Örneğin, DB adlı veritabanı katman sınıfım var. Bu sınıf, "Query (string $ sorgu)" adlı bir yönteme sahiptir, bu yöntem girdi üzerinde bir SQL sorgu dizesi alır. Bu sınıf (DB) için sahte oluşturabilir ve giriş sorgu dizesine bağlı olan farklı Sorgu yöntemi çağrıları için farklı dönüş değerleri ayarlayabilir miyim?


Aşağıdaki yanıta ek olarak, bu
yanıttaki

Yanıtlar:


132

PHPUnit Mocking kitaplığı (varsayılan olarak) bir beklentinin yalnızca expectsparametreye iletilen eşleştiriciye ve geçirilen kısıtlamaya göre eşleşip eşleşmediğini belirler method. Bu nedenle, expectyalnızca aktarılan bağımsız değişkenlerde farklı olan iki çağrı withbaşarısız olacaktır çünkü ikisi de eşleşecek ancak yalnızca biri beklenen davranışa sahip olduğunu doğrulayacaktır. Gerçek çalışma örneğinden sonra üreme durumuna bakın.


Sizin için problemi kullanmanız ->at()veya ->will($this->returnCallback(içinde belirtildiği gibi kullanmanız gerekir another question on the subject.

Misal:

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

çoğalır:

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)


Neden iki -> () çağrıları işe yaramıyor:

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

}

Sonuçlar

 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
yardımınız için teşekkürler! Cevabınız sorunumu tamamen çözdü. Not: Basit mimari için bu kadar büyük çözümler kullanmak zorunda kaldığımda bazen TDD geliştirme bana korkunç geliyor :)
Aleksei Kornushkin

1
Bu harika bir cevap, PHPUnit alaylarını anlamama gerçekten yardımcı oldu. Teşekkürler!!
Steve Bauman

İlgilendiğiniz bağımsız değişkenler için varsayılan bir değer sağlamanıza izin veren $this->anything()parametrelerden biri olarak da kullanabilirsiniz ->logicalOr().
MatsLindh

2
Hiç kimsenin bahsetmediğini merak ediyorum, "-> mantıksalOr ()" ile (bu durumda) her iki argümanın çağrıldığını garanti edemezsiniz. Yani bu gerçekten sorunu çözmez.
user3790897

184

at()Bundan kaçınabiliyorsanız kullanmak ideal değildir çünkü dokümanlarının iddia ettiği gibi

At () eşleştiricisi için $ indeks parametresi, belirli bir sahte nesne için tüm yöntem çağrılarında sıfırdan başlayan dizine başvurur. Bu eşleştiriciyi kullanırken, belirli uygulama ayrıntılarına çok yakından bağlı olan kırılgan testlere yol açabileceğinden dikkatli olun.

4.1'den beri withConsecutiveörneğin kullanabilirsiniz .

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

Ardışık aramalarda geri dönmesini istiyorsanız:

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

22
2016 itibariyle en iyi cevap. Kabul edilen cevaptan daha iyi.
Matthew Housser

Bu iki farklı parametre için farklı bir şey nasıl döndürülür?
Lenin Raj Rajasekaran

@emaillenin willReturnOnConsecutiveCalls kullanarak benzer bir şekilde.
xarlymg89

Bilginize, PHPUnit 4.0.20 kullanıyordum ve hata alıyordum Fatal error: Call to undefined method PHPUnit_Framework_MockObject_Builder_InvocationMocker::withConsecutive(), Composer ile bir çırpıda 4.1'e yükselttim ve çalışıyor.
değişim

willReturnOnConsecutiveCallsÖldürdü.
Rafael Barros

18

Bulduğuma göre, bu sorunu çözmenin en iyi yolu PHPUnit'in değer haritası işlevselliğini kullanmaktır.

PHPUnit'in belgelerinden örnek :

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

Bu test başarılı. Gördüğün gibi:

  • işlev "a" ve "b" parametreleriyle çağrıldığında, "d" döndürülür
  • işlev "e" ve "f" parametreleriyle çağrıldığında, "h" döndürülür

Söyleyebileceğim kadarıyla, bu özellik PHPUnit 3.6'da tanıtıldı , bu yüzden hemen hemen her türlü geliştirme veya aşamalandırma ortamında ve herhangi bir sürekli entegrasyon aracıyla güvenle kullanılabilecek kadar "eski".


6

Görünüşe göre Mockery ( https://github.com/padraic/mockery ) bunu destekliyor. Benim durumumda, bir veritabanında 2 indeksin oluşturulduğunu kontrol etmek istiyorum:

Alay, çalışır:

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, bu başarısız olur:

$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 ayrıca daha hoş bir sözdizimi IMHO'ya sahiptir. PHPUnits yerleşik alay yeteneği, ancak YMMV'den biraz daha yavaş görünüyor.


0

giriş

Tamam, Mockery için sağlanan bir çözüm olduğunu görüyorum, bu yüzden Mockery'den hoşlanmadığım için, size bir Prophecy alternatifi vereceğim, ancak önce Mockery ile Prophecy arasındaki farkı okumanızı öneririm .

Uzun lafın kısası : "Kehanet, mesaj bağlama adı verilen yaklaşımı kullanır - bu, yöntemin davranışının zamanla değişmediği, aksine diğer yöntem tarafından değiştirildiği anlamına gelir."

Kapsanacak gerçek dünya sorunlu kodu

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 çözümü

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

özet

Kehanet bir kez daha harika! Benim püf noktam Prophecy'nin ileti bağlama doğasından yararlanmak ve ne yazık ki tipik bir geri arama javascript cehennem kodu gibi görünse de, $ self = $ this;Çok nadiren böyle birim testleri yazmak zorunda kaldığınız için, bunun güzel bir çözüm olduğunu düşünüyorum ve programın yürütülmesini tanımladığı için takip etmesi, hata ayıklaması kesinlikle kolay.

BTW: İkinci bir alternatif var ancak test ettiğimiz kodu değiştirmeyi gerektiriyor. Sorun çıkaranları paketleyip ayrı bir sınıfa taşıyabiliriz:

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

şu şekilde sarılabilir:

$processorChunkStorage->persistChunkToInProgress($chunk);

ve bu kadar ama onun için başka bir sınıf oluşturmak istemediğim için ilkini tercih ediyorum.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.