phpunit método simulado múltiples llamadas con diferentes argumentos

117

¿Hay alguna forma de definir diferentes simulaciones de espera para diferentes argumentos de entrada? Por ejemplo, tengo una clase de capa de base de datos llamada DB. Esta clase tiene un método llamado "Consulta (cadena $ consulta)", ese método toma una cadena de consulta SQL en la entrada. ¿Puedo crear un simulacro para esta clase (DB) y establecer diferentes valores de retorno para diferentes llamadas al método de consulta que dependen de la cadena de consulta de entrada?

Aleksei Kornushkin
fuente
Además de la respuesta a continuación, también puede usar el método en esta respuesta: stackoverflow.com/questions/5484602/…
Schleis
Me gusta esta respuesta stackoverflow.com/a/10964562/614709
yitznewton

Respuestas:

131

La biblioteca PHPUnit Mocking (por defecto) determina si una expectativa coincide basándose únicamente en el comparador pasado al expectsparámetro y la restricción pasada method. Debido a esto, dos expectllamadas que solo difieren en los argumentos pasados withfallarán porque ambas coincidirán pero solo una verificará que tiene el comportamiento esperado. Vea el caso de reproducción después del ejemplo de trabajo real.


Para su problema, debe usar ->at()o ->will($this->returnCallback(como se describe en another question on the subject.

Ejemplo:

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

Reproduce:

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)


Reproduzca por qué dos -> con () llamadas no funcionan:

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

}

Resultados en

 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
edoriano
fuente
7
¡gracias por tu ayuda! Tu respuesta resolvió completamente mi problema. PD A veces, el desarrollo de TDD me parece aterrador cuando tengo que usar soluciones tan grandes para una arquitectura simple :)
Aleksei Kornushkin
1
Esta es una gran respuesta, realmente me ayudó a entender las burlas de PHPUnit. ¡¡Gracias!!
Steve Bauman
También puede usar $this->anything()como uno de los parámetros para ->logicalOr()permitirle proporcionar un valor predeterminado para otros argumentos que no sean el que le interesa.
MatsLindh
2
Me pregunto si nadie menciona, que con "-> logicOr ()" no garantizará que (en este caso) se hayan llamado ambos argumentos. Entonces esto realmente no resuelve el problema.
user3790897
182

No es ideal usarlo at()si puede evitarlo porque, como afirman sus documentos

El parámetro $ index para el comparador at () se refiere al índice, comenzando en cero, en todas las invocaciones de métodos para un objeto simulado dado. Tenga cuidado al utilizar este comparador, ya que puede dar lugar a pruebas frágiles que están demasiado estrechamente vinculadas a detalles de implementación específicos.

Desde 4.1 puede utilizar, withConsecutivepor ejemplo.

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

Si desea que vuelva en llamadas consecutivas:

  $mock->method('set')
         ->withConsecutive([$argA1, $argA2], [$argB1], [$argC1, $argC2])
         ->willReturnOnConsecutiveCalls($retValueA, $retValueB, $retValueC);
hirowatari
fuente
22
Mejor respuesta a partir de 2016. Respuesta mejor que aceptada.
Matthew Housser
¿Cómo devolver algo diferente para esos dos parámetros diferentes?
Lenin Raj Rajasekaran
@emaillenin usando willReturnOnConsecutiveCalls de manera similar.
xarlymg89
FYI, estaba usando PHPUnit 4.0.20 y recibí un error Fatal error: Call to undefined method PHPUnit_Framework_MockObject_Builder_InvocationMocker::withConsecutive(), actualicé a 4.1 en un instante con Composer y está funcionando.
quickshiftin
Lo willReturnOnConsecutiveCallsmató.
Rafael Barros
17

Por lo que he encontrado, la mejor manera de resolver este problema es utilizando la funcionalidad de mapa de valores de PHPUnit.

Ejemplo de la documentación de 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'));
    }
}

Esta prueba pasa. Como puedes ver:

  • cuando se llama a la función con los parámetros "a" y "b", se devuelve "d"
  • cuando se llama a la función con los parámetros "e" y "f", se devuelve "h"

Por lo que puedo decir, esta función se introdujo en PHPUnit 3.6 , por lo que es lo suficientemente "antigua" como para que se pueda utilizar de forma segura en prácticamente cualquier entorno de desarrollo o preparación y con cualquier herramienta de integración continua.

Radu Murzea
fuente
6

Parece que Mockery ( https://github.com/padraic/mockery ) apoya esto. En mi caso, quiero verificar que se creen 2 índices en una base de datos:

La burla, funciona:

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, esto falla:

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

La burla también tiene una sintaxis mejor en mi humilde opinión. Parece ser un poco más lento que la capacidad de burla incorporada de PHPUnit, pero YMMV.

joerx
fuente
0

Intro

Bien, veo que hay una solución para Mockery, así que como no me gusta Mockery, voy a darte una alternativa de Profecía, pero te sugiero que primero leas sobre la diferencia entre Mockery y Prophecy.

En pocas palabras : "La profecía utiliza un enfoque llamado enlace de mensajes ; significa que el comportamiento del método no cambia con el tiempo, sino que cambia con el otro método".

Código problemático del mundo real para cubrir

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

Solución 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);
    }
}

Resumen

Una vez más, ¡la profecía es más asombrosa! Mi truco consiste en aprovechar la naturaleza vinculante de mensajería de Prophecy y, aunque lamentablemente parece un código infernal de javascript de devolución de llamada típico, comenzando con $ self = $ this; Como rara vez tienes que escribir pruebas unitarias como esta, creo que es una buena solución y definitivamente es fácil de seguir, depurar, ya que en realidad describe la ejecución del programa.

Por cierto: hay una segunda alternativa, pero requiere cambiar el código que estamos probando. Podríamos envolver a los alborotadores y trasladarlos a una clase separada:

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

podría envolverse como:

$processorChunkStorage->persistChunkToInProgress($chunk);

y eso es todo, pero como no quería crear otra clase para él, prefiero la primera.

Lukas Lukac
fuente