Método simulado phpunit múltiplas chamadas com diferentes argumentos


117

Existe alguma maneira de definir diferentes expectativas de simulação para diferentes argumentos de entrada? Por exemplo, tenho uma classe de camada de banco de dados chamada DB. Esta classe possui um método chamado "Query (string $ query)", esse método recebe uma string de consulta SQL na entrada. Posso criar simulação para esta classe (banco de dados) e definir diferentes valores de retorno para diferentes chamadas de método de consulta que dependem da string de consulta de entrada?


Além da resposta abaixo, você também pode usar o método nesta resposta: stackoverflow.com/questions/5484602/…
Schleis

Respostas:


131

A biblioteca PHPUnit Mocking (por padrão) determina se uma expectativa corresponde com base unicamente no matcher passado para o expectsparâmetro e na restrição passada para method. Por causa disso, duas expectchamadas que diferem apenas nos argumentos passados ​​para withfalharão porque ambas corresponderão, mas apenas uma verificará como tendo o comportamento esperado. Veja a caixa de reprodução após o exemplo de trabalho real.


Para seu problema, você precisa usar ->at()ou ->will($this->returnCallback(conforme descrito em another question on the subject.

Exemplo:

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

Reproduz:

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)


Reproduza porque duas chamadas -> with () não funcionam:

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

}

Resulta em

 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
Obrigado pela ajuda! Sua resposta resolveu completamente meu problema. PS Às vezes, o desenvolvimento de TDD parece assustador para mim quando tenho que usar soluções tão grandes para arquitetura simples :)
Aleksei Kornushkin

1
Esta é uma ótima resposta, realmente me ajudou a entender os mocks do PHPUnit. Obrigado!!
Steve Bauman

Você também pode usar $this->anything()como um dos parâmetros para ->logicalOr()permitir que você forneça um valor padrão para outros argumentos além daquele em que você está interessado.
MatsLindh

2
Estou imaginando que ninguém mencione, que com "-> logicalOr ()" você não garante que (neste caso) ambos os argumentos foram chamados. Portanto, isso realmente não resolve o problema.
user3790897

182

Não é ideal para usar at()se você puder evitá-lo, porque como seus documentos afirmam

O parâmetro $ index para o matcher at () se refere ao índice, começando em zero, em todas as invocações de método para um determinado objeto mock. Tenha cuidado ao usar este matcher, pois ele pode levar a testes frágeis que estão intimamente ligados a detalhes de implementação específicos.

Desde 4.1 você pode usar, withConsecutivepor exemplo.

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

Se você quiser fazer com que ele retorne em ligações consecutivas:

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

22
Melhor resposta em 2016. Resposta melhor do que aceita.
Matthew Housser

Como retornar algo diferente para esses dois parâmetros diferentes?
Lenin Raj Rajasekaran

@emaillenin usando willReturnOnConsecutiveCalls de maneira semelhante.
xarlymg89

Para sua informação, eu estava usando o PHPUnit 4.0.20 e recebendo um erro Fatal error: Call to undefined method PHPUnit_Framework_MockObject_Builder_InvocationMocker::withConsecutive(), atualizei para 4.1 em um piscar de olhos com o Composer e está funcionando.
quickshift em

O willReturnOnConsecutiveCallsmatou.
Rafael Barros

17

Pelo que descobri, a melhor maneira de resolver esse problema é usando a funcionalidade de mapa de valor do PHPUnit.

Exemplo da documentação do 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'));
    }
}

Este teste é aprovado. Como você pode ver:

  • quando a função é chamada com os parâmetros "a" e "b", "d" é retornado
  • quando a função é chamada com os parâmetros "e" e "f", "h" é retornado

Pelo que posso dizer, esse recurso foi introduzido no PHPUnit 3.6 , então é "antigo" o suficiente para que possa ser usado com segurança em praticamente qualquer ambiente de desenvolvimento ou teste e com qualquer ferramenta de integração contínua.


6

Parece Mockery ( https://github.com/padraic/mockery ) suporta isso. No meu caso, quero verificar se 2 índices são criados em um banco de dados:

Zombaria, 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, falha:

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

Zombaria também tem uma sintaxe IMHO melhor. Parece ser um pouco mais lento do que a capacidade de mocking embutida do PHPUnits, mas YMMV.


0

Introdução

Ok, vejo que existe uma solução fornecida para Mockery, então como eu não gosto de Mockery, vou dar a você uma alternativa de Profecia, mas eu sugiro que você primeiro leia sobre a diferença entre Mockery e Prophecy.

Resumindo : "A profecia usa uma abordagem chamada vinculação de mensagens - significa que o comportamento do método não muda com o tempo, mas sim pelo outro método."

Código problemático do mundo real para cobrir

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

Solução 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);
    }
}

Resumo

Mais uma vez, Prophecy é mais incrível! Meu truque é alavancar a natureza de ligação de mensagens do Prophecy e, mesmo que infelizmente pareça um código de inferno javascript de retorno de chamada típico, começando com $ self = $ this; como você raramente precisa escrever testes de unidade como este, acho que é uma boa solução e é definitivamente fácil de seguir, depurar, pois realmente descreve a execução do programa.

BTW: há uma segunda alternativa, mas requer a alteração do código que estamos testando. Poderíamos envolver os criadores de problemas e movê-los para uma classe separada:

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

pode ser embrulhado como:

$processorChunkStorage->persistChunkToInProgress($chunk);

e é isso mas como não queria criar outra turma para isso, prefiro a primeira.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.