phpunit mock method несколько вызовов с разными аргументами


117

Есть ли способ определить разные ложные ожидания для разных входных аргументов? Например, у меня есть класс уровня базы данных под названием DB. В этом классе есть метод под названием «Query (string $ query)», который принимает на входе строку запроса SQL. Могу ли я создать макет для этого класса (БД) и установить разные возвращаемые значения для разных вызовов методов запроса, которые зависят от входной строки запроса?


В дополнение к приведенному ниже ответу вы также можете использовать метод из этого ответа: stackoverflow.com/questions/5484602/…
Schleis

Мне нравится этот ответ stackoverflow.com/a/10964562/614709
yitznewton

Ответы:


132

Библиотека Mocking PHPUnit (по умолчанию) определяет, совпадает ли ожидание, основываясь исключительно на сопоставлении, переданном expectsпараметру, и ограничению, переданному в method. Из-за этого два expectвызова, которые отличаются только переданными аргументами with, потерпят неудачу, потому что оба будут совпадать, но только один будет подтвержден как имеющий ожидаемое поведение. См. Репродукцию после реального рабочего примера.


Для вашей проблемы вам нужно использовать ->at()или, ->will($this->returnCallback(как указано в another question on the subject.

Пример:

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

Воспроизводит:

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)


Воспроизведите, почему два вызова -> with () не работают:

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

}

Результаты в

 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
Спасибо за вашу помощь! Ваш ответ полностью решил мою проблему. PS Иногда разработка TDD кажется мне ужасной, когда мне приходится использовать такие большие решения для простой архитектуры :)
Алексей Корнушкин

1
Это отличный ответ, который действительно помог мне понять насмешки PHPUnit. Спасибо!!
Стив Бауман

Вы также можете использовать в $this->anything()качестве одного из параметров, чтобы ->logicalOr()позволить вам предоставить значение по умолчанию для других аргументов, кроме того, который вас интересует.
MatsLindh

2
Интересно, что никто не упоминает, что с "-> logicalOr ()" вы не гарантируете, что (в этом случае) были вызваны оба аргумента. Так что это не решает проблему.
user3790897

184

Это не идеально для использования, at()если вы можете избежать этого, потому что, как утверждают их документы

Параметр $ index для сопоставления at () относится к индексу, начиная с нуля, во всех вызовах метода для данного фиктивного объекта. Соблюдайте осторожность при использовании этого сопоставления, поскольку это может привести к нестабильным тестам, которые слишком тесно связаны с конкретными деталями реализации.

Начиная с версии 4.1 вы можете использовать, withConsecutiveнапример.

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

Если вы хотите, чтобы он возвращался при последовательных вызовах:

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

22
Лучший ответ по состоянию на 2016 год. Лучше, чем принятый ответ.
Matthew Housser

Как вернуть что-то другое для этих двух разных параметров?
Lenin Raj Rajasekaran

@emaillenin аналогичным образом использует willReturnOnConsecutiveCalls.
xarlymg89

FYI, я использовал PHPUnit 4.0.20 и получал ошибку Fatal error: Call to undefined method PHPUnit_Framework_MockObject_Builder_InvocationMocker::withConsecutive(), обновился до 4.1 в мгновение ока с помощью Composer, и он работает.
quickshiftin

willReturnOnConsecutiveCallsУбил его.
Рафаэль Баррос

18

Из того, что я нашел, лучший способ решить эту проблему - использовать функциональность карты значений PHPUnit.

Пример из документации 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'));
    }
}

Этот тест проходит. Как вы видете:

  • когда функция вызывается с параметрами "a" и "b", возвращается "d"
  • когда функция вызывается с параметрами "e" и "f", возвращается "h"

Насколько я могу судить, эта функция была введена в PHPUnit 3.6 , поэтому она достаточно «старая», чтобы ее можно было безопасно использовать практически в любых средах разработки или промежуточных средах и с любым инструментом непрерывной интеграции.


6

Кажется, Mockery ( https://github.com/padraic/mockery ) поддерживает это. В моем случае я хочу проверить, что в базе данных создано 2 индекса:

Издевательство, работает:

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, это не удается:

$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 более приятный синтаксис. Похоже, что это немного медленнее, чем встроенная функция имитации PHPUnits, но YMMV.


0

вступление

Хорошо, я вижу, что есть одно решение для Mockery, поэтому, поскольку мне не нравится Mockery, я собираюсь дать вам альтернативу Prophecy, но я предлагаю вам сначала прочитать о разнице между Mockery и Prophecy.

Короче говоря : «Prophecy использует подход, называемый привязкой сообщения - это означает, что поведение метода не меняется со временем, а скорее изменяется другим методом».

Реальный проблемный код для покрытия

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

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

Резюме

Еще раз, Пророчество еще круче! Моя уловка состоит в том, чтобы использовать природу связывания сообщений в Prophecy, и хотя это, к сожалению, выглядит как типичный адский код обратного вызова javascript, начинающийся с $ self = $ this; поскольку вам очень редко приходится писать подобные модульные тесты, я думаю, что это хорошее решение, и за ним, безусловно, легко следить, отлаживать, поскольку оно фактически описывает выполнение программы.

BTW: есть вторая альтернатива, но она требует изменения кода, который мы тестируем. Мы могли бы обернуть смутьянов и вынести их в отдельный класс:

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

можно обернуть как:

$processorChunkStorage->persistChunkToInProgress($chunk);

и все, но поскольку я не хотел создавать для него еще один класс, я предпочитаю первый.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.