phpunit mock metodo più chiamate con argomenti diversi


117

C'è un modo per definire diverse finte aspettative per diversi argomenti di input? Ad esempio, ho una classe di livello di database chiamata DB. Questa classe ha un metodo chiamato "Query (string $ query)", che accetta una stringa di query SQL in input. Posso creare mock per questa classe (DB) e impostare diversi valori di ritorno per diverse chiamate al metodo Query che dipendono dalla stringa di query di input?


In aggiunta alla risposta qui sotto, è anche possibile utilizzare il metodo in questa risposta: stackoverflow.com/questions/5484602/...
Clusio

Risposte:


132

La libreria PHPUnit Mocking (per impostazione predefinita) determina se un'aspettativa corrisponde in base esclusivamente al matcher passato al expectsparametro e al vincolo passato a method. Per questo motivo , due expectchiamate che differiscono solo negli argomenti passati withavranno esito negativo perché entrambe corrisponderanno, ma solo una verificherà il comportamento previsto. Vedere il caso di riproduzione dopo l'esempio di funzionamento effettivo.


Per il tuo problema devi usare ->at()o ->will($this->returnCallback(come descritto in another question on the subject.

Esempio:

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

riproduce:

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)


Riprodurre perché due -> chiamate con () non funzionano:

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

}

Risultati 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

7
Grazie per l'aiuto! La tua risposta ha completamente risolto il mio problema. PS A volte lo sviluppo di TDD mi sembra terrificante quando devo usare soluzioni così grandi per un'architettura semplice :)
Aleksei Kornushkin

1
Questa è un'ottima risposta, mi ha davvero aiutato a capire gli scherzi di PHPUnit. Grazie!!
Steve Bauman

Puoi anche utilizzare $this->anything()come uno dei parametri per ->logicalOr()consentirti di fornire un valore predefinito per argomenti diversi da quello che ti interessa.
MatsLindh

2
Mi chiedo che nessuno menzioni che con "-> logicalOr ()" non garantirai che (in questo caso) entrambi gli argomenti siano stati chiamati. Quindi questo non risolve davvero il problema.
user3790897

182

Non è l'ideale da usare at()se puoi evitarlo perché come affermano i loro documenti

Il parametro $ index per il matcher at () si riferisce all'indice, a partire da zero, in tutte le invocazioni di metodi per un dato oggetto mock. Prestare attenzione quando si utilizza questo matcher in quanto può portare a test fragili che sono troppo strettamente legati a dettagli di implementazione specifici.

Dalla 4.1 puoi usare withConsecutivead es.

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

Se vuoi farlo tornare su chiamate consecutive:

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

22
Migliore risposta del 2016. Risposta migliore di quella accettata.
Matthew Housser

Come restituire qualcosa di diverso per questi due diversi parametri?
Lenin Raj Rajasekaran

@emaillenin utilizzando willReturnOnConsecutiveCalls in modo simile.
xarlymg89

Cordiali saluti, stavo usando PHPUnit 4.0.20 e ricevevo un errore Fatal error: Call to undefined method PHPUnit_Framework_MockObject_Builder_InvocationMocker::withConsecutive(), aggiornato a 4.1 in un attimo con Composer e funziona.
cambio rapido:

L'hanno willReturnOnConsecutiveCallsucciso.
Rafael Barros

17

Da quello che ho scoperto, il modo migliore per risolvere questo problema è utilizzare la funzionalità di mappatura dei valori di PHPUnit.

Esempio dalla documentazione di 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'));
    }
}

Questo test viene superato. Come potete vedere:

  • quando la funzione viene chiamata con i parametri "a" e "b", viene restituito "d"
  • quando la funzione viene chiamata con i parametri "e" e "f", viene restituito "h"

Da quello che posso dire, questa funzionalità è stata introdotta in PHPUnit 3.6 , quindi è abbastanza "vecchia" da poter essere utilizzata in sicurezza praticamente su qualsiasi ambiente di sviluppo o di gestione temporanea e con qualsiasi strumento di integrazione continua.


6

Sembra che Mockery ( https://github.com/padraic/mockery ) lo supporti. Nel mio caso voglio verificare che su un database vengano creati 2 indici:

Mockery, funziona:

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, questo non riesce:

$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 ha anche una sintassi più carina IMHO. Sembra essere un po 'più lento della capacità di mocking incorporata di PHPUnits, ma YMMV.


0

Intro

Ok, vedo che c'è una soluzione fornita per Mockery, quindi dato che non mi piace Mockery, ti offrirò un'alternativa a Prophecy ma ti suggerirei prima di leggere prima la differenza tra Mockery e Prophecy.

Per farla breve : "La profezia utilizza un approccio chiamato binding dei messaggi - significa che il comportamento del metodo non cambia nel tempo, ma piuttosto viene modificato dall'altro metodo".

Codice problematico del mondo reale da coprire

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

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

Sommario

Ancora una volta, Prophecy è più fantastico! Il mio trucco è sfruttare la natura vincolante dei messaggi di Prophecy e anche se purtroppo sembra un tipico codice javascript di callback, che inizia con $ self = $ this; dato che molto raramente devi scrivere unit test come questo, penso che sia una buona soluzione ed è sicuramente facile da seguire, eseguire il debug, poiché in realtà descrive l'esecuzione del programma.

BTW: C'è una seconda alternativa ma richiede la modifica del codice che stiamo testando. Potremmo avvolgere i piantagrane e spostarli in una classe separata:

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

potrebbe essere avvolto come:

$processorChunkStorage->persistChunkToInProgress($chunk);

e basta, ma poiché non volevo creare un'altra classe per questo, preferisco la prima.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.