phpunit mock metoda wielokrotnych wywołań z różnymi argumentami

117

Czy istnieje sposób na zdefiniowanie różnych pozornych oczekiwań dla różnych argumentów wejściowych? Na przykład mam klasę warstwy bazy danych o nazwie DB. Ta klasa ma metodę o nazwie „Query (string $ query)”, która pobiera ciąg zapytania SQL na wejściu. Czy mogę utworzyć makietę dla tej klasy (DB) i ustawić różne wartości zwracane dla różnych wywołań metod Query, które zależą od wejściowego ciągu zapytania?

Aleksei Kornushkin
źródło
Oprócz odpowiedzi poniżej, możesz również użyć metody w tej odpowiedzi: stackoverflow.com/questions/5484602/ ...
Schleis Kwietnia
Podoba mi się ta odpowiedź stackoverflow.com/a/10964562/614709
yitznewton

Odpowiedzi:

132

Biblioteka PHPUnit Mocking (domyślnie) określa, czy oczekiwanie pasuje wyłącznie na podstawie dopasowania przekazanego do expectsparametru i ograniczenia przekazanego do method. Z tego powodu dwa expectwywołania, które różnią się tylko argumentami przekazanymi do, withzakończą się niepowodzeniem, ponieważ oba będą pasować, ale tylko jedno zostanie sprawdzone jako mające oczekiwane zachowanie. Zobacz przypadek reprodukcji po rzeczywistym przykładzie roboczym.


W przypadku problemu należy użyć ->at()lub ->will($this->returnCallback(zgodnie z opisem w another question on the subject.

Przykład:

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

Odtwarza:

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)


Powtórz, dlaczego dwa -> z wywołaniami () nie działają:

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

}

Prowadzi do

 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
źródło
7
dzięki za pomoc! Twoja odpowiedź całkowicie rozwiązała mój problem. PS Czasami rozwój TDD wydaje mi się przerażający, kiedy muszę używać tak dużych rozwiązań dla prostej architektury :)
Aleksei Kornushkin
1
To świetna odpowiedź, naprawdę pomogła mi zrozumieć kpiny z PHPUnit. Dzięki!!
Steve Bauman
Możesz również użyć $this->anything()jako jednego z parametrów, aby ->logicalOr()umożliwić podanie wartości domyślnej dla innych argumentów niż ten, który Cię interesuje.
MatsLindh
2
Dziwię się, że nikt nie wspomina, że ​​z "-> logicalOr ()" nie zagwarantujesz, że (w tym przypadku) oba argumenty zostały wywołane. Więc to tak naprawdę nie rozwiązuje problemu.
user3790897
183

Nie jest to idealne rozwiązanie, at()jeśli możesz tego uniknąć, ponieważ jak twierdzą ich doktorzy

Parametr $ index dla dopasowania at () odnosi się do indeksu, zaczynając od zera, we wszystkich wywołaniach metod dla danego obiektu pozorowanego. Zachowaj ostrożność podczas korzystania z tego dopasowania, ponieważ może to prowadzić do kruchych testów, które są zbyt ściśle powiązane z konkretnymi szczegółami implementacji.

Od 4.1 możesz używać withConsecutivenp.

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

Jeśli chcesz, aby powracał przy kolejnych połączeniach:

  $mock->method('set')
         ->withConsecutive([$argA1, $argA2], [$argB1], [$argC1, $argC2])
         ->willReturnOnConsecutiveCalls($retValueA, $retValueB, $retValueC);
hirowatari
źródło
22
Najlepsza odpowiedź z 2016 r. Lepsza niż zaakceptowana odpowiedź.
Matthew Housser
Jak zwrócić coś innego dla tych dwóch różnych parametrów?
Lenin Raj Rajasekaran
@emaillenin używając willReturnOnConsecutiveCalls w podobny sposób.
xarlymg89
FYI, używałem PHPUnit 4.0.20 i otrzymałem błąd Fatal error: Call to undefined method PHPUnit_Framework_MockObject_Builder_InvocationMocker::withConsecutive(), zaktualizowałem do 4.1 w mgnieniu oka z Composerem i działa.
quickshiftin
willReturnOnConsecutiveCallsZabił go.
Rafael Barros
17

Z tego, co znalazłem, najlepszym sposobem rozwiązania tego problemu jest użycie funkcji mapy wartości PHPUnit.

Przykład z dokumentacji 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'));
    }
}

Ten test przechodzi pomyślnie. Jak widzisz:

  • gdy wywoływana jest funkcja z parametrami „a” i „b”, zwracane jest „d”
  • gdy wywoływana jest funkcja z parametrami „e” i „f”, zwracane jest „h”

Z tego co wiem, ta funkcja została wprowadzona w PHPUnit 3.6 , więc jest na tyle „stara”, że można ją bezpiecznie używać w prawie każdym środowisku programowania lub przejściowym oraz z dowolnym narzędziem do ciągłej integracji.

Radu Murzea
źródło
6

Wygląda na to, że Mockery ( https://github.com/padraic/mockery ) to obsługuje. W moim przypadku chcę sprawdzić, czy w bazie danych są utworzone 2 indeksy:

Kpina, działa:

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, to nie działa:

$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 ma również ładniejszą składnię IMHO. Wygląda na to, że jest odrobinę wolniejszy niż wbudowana funkcja mockowania PHPUnits, ale YMMV.

joerx
źródło
0

Intro

Okay, widzę, że jest jedno rozwiązanie przewidziane dla Mockery, więc nie lubię Mockery, mam zamiar dać ci alternatywę dla Proroctwa, ale proponuję, abyś najpierw przeczytał o różnicy między Mockery a Prophecy.

Krótko mówiąc : „Prophecy używa podejścia zwanego wiązaniem wiadomości - oznacza to, że zachowanie metody nie zmienia się w czasie, ale jest raczej zmieniane przez inną metodę”.

Prawdziwy problematyczny kod do pokrycia

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

Rozwiązanie 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);
    }
}

Podsumowanie

Po raz kolejny Prophecy jest bardziej niesamowite! Moja sztuczka polega na wykorzystaniu charakteru Proroctwa wiążącego wiadomości i nawet jeśli wygląda to niestety jak typowy, zwrotny kod javascript, zaczynający się od $ self = $ this; ponieważ bardzo rzadko trzeba pisać testy jednostkowe, takie jak ten, myślę, że jest to fajne rozwiązanie i zdecydowanie łatwe do naśladowania, debugowania, ponieważ faktycznie opisuje wykonanie programu.

BTW: Istnieje druga alternatywa, ale wymaga zmiany kodu, który testujemy. Moglibyśmy opakować wichrzycieli i przenieść ich do osobnej klasy:

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

można opakować jako:

$processorChunkStorage->persistChunkToInProgress($chunk);

i to wszystko, ale ponieważ nie chciałem tworzyć dla niego kolejnej klasy, wolę pierwszą.

Lukas Lukac
źródło