phpunit mock метод декількох викликів з різними аргументами


117

Чи є спосіб визначити різні макетні очікування для різних вхідних аргументів? Наприклад, у мене є клас рівня баз даних під назвою DB. Цей клас має метод, який називається "Запит (рядок $ запит)", цей метод приймає рядок запиту SQL на вході. Чи можу я створити макет для цього класу (БД) і встановити різні значення повернення для різних викликів методу запиту, що залежить від вхідного рядка запиту?


На додаток до наведеного нижче відповідь, ви можете також використовувати метод в цій відповіді: stackoverflow.com/questions/5484602 / ...
Schleis

Мені подобається ця відповідь stackoverflow.com/a/10964562/614709
yitznewton

Відповіді:


131

Бібліотека глузування 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)


Відтворити, чому два -> з () дзвінками не працюють:

<?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
Цікаво, що ніхто не згадує, що з "-> logicOr ()" ви не гарантуєте, що (в даному випадку) були викликані обидва аргументи. Тож це насправді не вирішує проблему.
користувач3790897

182

Це не ідеально використовувати, 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 р. Краще, ніж прийнята відповідь.
Метью Гуссер

Як повернути щось різне для цих двох різних параметрів?
Ленін Радж Раджасекаран

@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Убив його.
Рафаель Баррос

17

З того, що я знайшов, найкращий спосіб вирішити цю проблему - це використовувати функціональну карту цінності 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

Здається, насмішка підтримує насмішку ( 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);

PHPУ цьому не виходить:

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

Знущання також мають приємніший синтаксис IMHO. Схоже, це в рази менше, ніж PHPUnits з вбудованою функцією глузування, але YMMV.


0

Вступ

Гаразд, я бачу, що для насмішки є одне рішення, тому, як мені не подобається насмішка, я збираюся дати вам альтернативу пророцтва, але я б запропонував вам спочатку прочитати про різницю між насмішкою та пророцтвом.

Довга коротка історія : "Пророцтво використовує підхід, який називається прив'язкою повідомлень - це означає, що поведінка методу не змінюється з часом, а скоріше змінюється іншим методом".

Реальний проблемний код, який слід висвітлити

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

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

Підсумок

Ще раз, Пророцтво є приголомшливішим! Моя хитрість полягає в тому, щоб використовувати характер обміну повідомленнями пророцтва, і хоча це, на жаль, схоже на типовий пекельний код JavaScript у зворотному дзвінку, починаючи з $ self = $ this; так як вам дуже рідко доводиться писати одиничні тести на кшталт цього, я думаю, що це приємне рішення, і це, безумовно, легко прослідкувати, налагодження, як це фактично описує виконання програми.

BTW: Є друга альтернатива, але вимагає змінити код, який ми тестуємо. Ми можемо перетворити неполадок і перенести їх в окремий клас:

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

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

$processorChunkStorage->persistChunkToInProgress($chunk);

і це все, але оскільки я не хотів створювати для нього ще один клас, я віддаю перевагу першому.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.