phpunit mock method mehrere aufrufe mit unterschiedlichen argumenten

117

Gibt es eine Möglichkeit, unterschiedliche Scheinerwartungen für unterschiedliche Eingabeargumente zu definieren? Zum Beispiel habe ich eine Datenbankschichtklasse namens DB. Diese Klasse hat die Methode "Query (string $ query)". Diese Methode verwendet bei der Eingabe eine SQL-Abfragezeichenfolge. Kann ich für diese Klasse (DB) ein Modell erstellen und unterschiedliche Rückgabewerte für verschiedene Aufrufe von Abfragemethoden festlegen, die von der Eingabe-Abfragezeichenfolge abhängen?

Aleksei Kornushkin
quelle
Zusätzlich zu der Antwort unten können Sie auch die Methode in dieser Antwort verwenden: stackoverflow.com/questions/5484602/…
Schleis
Ich mag diese Antwort stackoverflow.com/a/10964562/614709
yitznewton

Antworten:

131

Die PHPUnit Mocking-Bibliothek (standardmäßig) bestimmt, ob eine Erwartung nur anhand des an den expectsParameter übergebenen Matchers und der an übergebenen Einschränkung übereinstimmt method. Aus diesem Grund schlagen zwei expectAufrufe withfehl , die sich nur in den übergebenen Argumenten unterscheiden , da beide übereinstimmen, aber nur einer das erwartete Verhalten aufweist. Siehe den Reproduktionsfall nach dem tatsächlichen Arbeitsbeispiel.


Für Ihr Problem müssen Sie verwenden ->at()oder ->will($this->returnCallback(wie in beschrieben another question on the subject.

Beispiel:

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

Reproduziert:

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)


Reproduzieren Sie, warum zwei -> with () -Aufrufe nicht funktionieren:

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

}

Ergebnisse in

 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
quelle
7
danke für Ihre Hilfe! Ihre Antwort hat mein Problem vollständig gelöst. PS Manchmal scheint mir die TDD-Entwicklung erschreckend, wenn ich so große Lösungen für einfache Architektur verwenden muss :)
Aleksei Kornushkin
1
Dies ist eine großartige Antwort, die mir wirklich geholfen hat, PHPUnit-Mocks zu verstehen. Vielen Dank!!
Steve Bauman
Sie können auch $this->anything()als einen der Parameter verwenden, ->logicalOr()um einen Standardwert für andere Argumente als dasjenige bereitzustellen, an dem Sie interessiert sind.
MatsLindh
2
Ich frage mich, dass niemand erwähnt, dass Sie mit "-> LogicOr ()" nicht garantieren können, dass (in diesem Fall) beide Argumente aufgerufen wurden. Das löst das Problem also nicht wirklich.
user3790897
182

Es ist nicht ideal zu verwenden, at()wenn Sie es vermeiden können, weil, wie ihre Dokumente behaupten

Der Parameter $ index für den at () -Matcher bezieht sich auf den Index, beginnend bei Null, in allen Methodenaufrufen für ein bestimmtes Scheinobjekt. Seien Sie vorsichtig, wenn Sie diesen Matcher verwenden, da dies zu Sprödigkeitstests führen kann, die zu eng mit bestimmten Implementierungsdetails verknüpft sind.

Seit 4.1 können Sie withConsecutivezB verwenden.

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

Wenn Sie möchten, dass es bei aufeinanderfolgenden Anrufen zurückkehrt:

  $mock->method('set')
         ->withConsecutive([$argA1, $argA2], [$argB1], [$argC1, $argC2])
         ->willReturnOnConsecutiveCalls($retValueA, $retValueB, $retValueC);
Hirowatari
quelle
22
Beste Antwort ab 2016. Besser als akzeptierte Antwort.
Matthew Housser
Wie kann man für diese beiden unterschiedlichen Parameter etwas anderes zurückgeben?
Lenin Raj Rajasekaran
@emaillenin mit willReturnOnConsecutiveCalls auf ähnliche Weise.
Xarlymg89
Zu Ihrer Information, ich habe PHPUnit 4.0.20 verwendet und einen Fehler erhalten Fatal error: Call to undefined method PHPUnit_Framework_MockObject_Builder_InvocationMocker::withConsecutive(), der im Handumdrehen mit Composer auf 4.1 aktualisiert wurde und funktioniert.
Quickshiftin
Die willReturnOnConsecutiveCallshaben es getötet.
Rafael Barros
17

Soweit ich herausgefunden habe, ist der beste Weg, um dieses Problem zu lösen, die Verwendung der Value-Map-Funktionalität von PHPUnit.

Beispiel aus der Dokumentation von 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'));
    }
}

Dieser Test besteht. Wie du siehst:

  • Wenn die Funktion mit den Parametern "a" und "b" aufgerufen wird, wird "d" zurückgegeben
  • Wenn die Funktion mit den Parametern "e" und "f" aufgerufen wird, wird "h" zurückgegeben

Soweit ich weiß , wurde diese Funktion in PHPUnit 3.6 eingeführt . Sie ist also "alt" genug, um in nahezu jeder Entwicklungs- oder Staging-Umgebung und mit jedem Tool für die kontinuierliche Integration sicher verwendet werden zu können.

Radu Murzea
quelle
6

Es scheint, dass Mockery ( https://github.com/padraic/mockery ) dies unterstützt. In meinem Fall möchte ich überprüfen, ob 2 Indizes in einer Datenbank erstellt wurden:

Spott, funktioniert:

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, dies schlägt fehl:

$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 hat meiner Meinung nach auch eine schönere Syntax. Es scheint ein bisschen langsamer zu sein als die in PHPUnits integrierte Verspottungsfunktion, aber YMMV.

Joerx
quelle
0

Intro

Okay, ich sehe, dass es eine Lösung für Mockery gibt. Da ich Mockery nicht mag, werde ich Ihnen eine Prophezeiungsalternative geben, aber ich würde Ihnen empfehlen, zuerst über den Unterschied zwischen Spott und Prophezeiung zu lesen.

Lange Rede, kurzer Sinn: "Prophecy verwendet einen Ansatz, der als Nachrichtenbindung bezeichnet wird. Dies bedeutet, dass sich das Verhalten der Methode nicht im Laufe der Zeit ändert, sondern durch die andere Methode."

Problematischer Code der realen Welt

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 Lösung

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

Zusammenfassung

Wieder einmal ist Prophezeiung fantastischer! Mein Trick besteht darin, die Nachrichtenbindung von Prophecy zu nutzen, und obwohl es leider wie ein typischer Rückruf-Javascript-Höllencode aussieht, beginnend mit $ self = $ this; Da Sie sehr selten Unit-Tests wie diesen schreiben müssen, denke ich, dass dies eine gute Lösung ist und es definitiv einfach ist, zu folgen, zu debuggen, da es tatsächlich die Programmausführung beschreibt.

Übrigens: Es gibt eine zweite Alternative, die jedoch eine Änderung des zu testenden Codes erfordert. Wir könnten die Unruhestifter einwickeln und sie in eine separate Klasse bringen:

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

könnte verpackt werden als:

$processorChunkStorage->persistChunkToInProgress($chunk);

und das war's, aber da ich keine weitere Klasse dafür erstellen wollte, bevorzuge ich die erste.

Lukas Lukac
quelle