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

9 minutos de lectura

avatar de usuario
Alekséi Kornushkin

¿Hay alguna forma de definir diferentes expectativas simuladas 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 dependan de la cadena de consulta de entrada?

  • Además de la respuesta a continuación, también puede usar el método en esta respuesta: stackoverflow.com/questions/5484602/…

    – Schleis

    08/04/2013 a las 15:32

  • Me gusta esta respuesta stackoverflow.com/a/10964562/614709

    – yitznewton

    24 de enero de 2014 a las 13:38

avatar de usuario
hirowatari

No es ideal para usar at() si puedes 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 usar este comparador, ya que puede dar lugar a pruebas frágiles que están demasiado vinculadas a detalles de implementación específicos.

Desde 4.1 puedes usar withConsecutive p.ej.

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

Si desea que regrese en llamadas consecutivas:

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

  • Mejor respuesta a partir de 2016. Mejor que la respuesta aceptada.

    – Mateo Housser

    18 de marzo de 2016 a las 1:49

  • ¿Cómo devolver algo diferente para esos dos parámetros diferentes?

    –Lenin Raj Rajasekaran

    26 de julio de 2016 a las 4:41

  • @emaillenin usando willReturnOnConsecutiveCalls de manera similar.

    – xarlymg89

    29 de julio de 2016 a las 11:29

  • FYI, estaba usando PHPUnit 4.0.20 y recibí un error Fatal error: Call to undefined method PHPUnit_Framework_MockObject_Builder_InvocationMocker::withConsecutive()actualizado a 4.1 en un instante con Composer y está funcionando.

    – cambio rápido

    1 de marzo de 2017 a las 22:07

  • los willReturnOnConsecutiveCalls lo mató.

    –Rafael Barros

    7 julio 2017 a las 18:24

avatar de usuario
edoriano

La biblioteca PHPUnit Mocking (de forma predeterminada) determina si una expectativa coincide basándose únicamente en el comparador pasado. expects parámetro y la restricción pasada a method. Debido a esto, dos expect llamadas que solo difieren en los argumentos pasados ​​a with fallará porque ambos coincidirán, pero solo uno verificará que tiene el comportamiento esperado. Vea el caso de reproducción después del ejemplo de trabajo real.


Para tu problema necesitas 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 llamadas ->with() 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

  • ¡Gracias por tu ayuda! Tu respuesta resolvió por completo mi problema. PD A veces, el desarrollo de TDD me parece aterrador cuando tengo que usar soluciones tan grandes para una arquitectura simple 🙂

    – Alekséi Kornushkin

    13 de mayo de 2011 a las 12:38


  • Esta es una gran respuesta, realmente me ayudó a entender las simulaciones de PHPUnit. ¡¡Gracias!!

    –Steve Bauman

    22 de mayo de 2016 a las 4:18


  • También puedes usar $this->anything() como uno de los parámetros para ->logicalOr() para permitirle proporcionar un valor predeterminado para otros argumentos distintos al que le interesa.

    – Mats Lindh

    27 de enero de 2017 a las 13:31

  • Me pregunto si nadie menciona que con “->logicalOr()” no garantizará que (en este caso) ambos argumentos hayan sido llamados. Así que esto realmente no resuelve el problema.

    – usuario3790897

    24 de julio de 2017 a las 13:04


avatar de usuario
Radu Murzea

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

Ejemplo de 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 Unidad PHP 3.6por lo que es lo suficientemente “antiguo” como para que se pueda usar de forma segura en prácticamente cualquier entorno de desarrollo o ensayo y con cualquier herramienta de integración continua.

Parece Burla (https://github.com/padraic/mockery) apoya esto. En mi caso quiero comprobar que se crean 2 índices en una base de datos:

Burla, obras:

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 más agradable en mi humilde opinión. Parece ser un poco más lento que la capacidad de simulación incorporada de PHPUnit, pero YMMV.

avatar de usuario
lukas lukac

Introducción

De acuerdo, veo que se proporciona una solución para Mockery, así que como no me gusta Mockery, le daré una alternativa de Prophecy, pero le sugiero que primero lea primero sobre la diferencia entre burla y profecía.

Larga historia corta: “La profecía usa un enfoque llamado vinculación de mensajes – significa que el comportamiento del método no cambia con el tiempo, sino que lo cambia 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, Prophecy es más asombrosa! Mi truco es aprovechar la naturaleza vinculante de mensajería de Prophecy y, aunque lamentablemente parece un código de javascript de devolución de llamada típico, comenzando con $yo = $esto; Como rara vez tiene 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 moverlos 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.

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad