Mock в PHPUnit - множественная конфигурация одного и того же метода с разными аргументами

Можно ли настроить PHPUnit mock таким образом?

$context = $this->getMockBuilder('Context')
   ->getMock();

$context->expects($this->any())
   ->method('offsetGet')
   ->with('Matcher')
   ->will($this->returnValue(new Matcher()));

$context->expects($this->any())
   ->method('offsetGet')
   ->with('Logger')
   ->will($this->returnValue(new Logger()));

Я использую PHPUnit 3.5.10, и он терпит неудачу, когда я запрашиваю Matcher, потому что он ожидает аргумент "Logger". Это похоже на то, что второе ожидание переписывает первый, но когда я отказываюсь от макета, все выглядит нормально.

Ответ 1

Начиная с PHPUnit 3.6 существует $this->returnValueMap(), который может использоваться для возврата разных значений в зависимости от заданных параметров к заглушке метода.

Ответ 2

К сожалению, это невозможно при использовании PHPUnit Mock API по умолчанию.

Я вижу два варианта, которые могут приблизить вас к такому:

Использование → at ($ x)

$context = $this->getMockBuilder('Context')
   ->getMock();

$context->expects($this->at(0))
   ->method('offsetGet')
   ->with('Matcher')
   ->will($this->returnValue(new Matcher()));

$context->expects($this->at(1))
   ->method('offsetGet')
   ->with('Logger')
   ->will($this->returnValue(new Logger()));

Это будет работать нормально, но вы тестируете больше, чем должны (в основном, он сначала вызывается с помощью соединителя, и это деталь реализации).

И это не удастся, если у вас есть несколько вызовов для каждой из функций!


Принятие обоих параметров и использование returnCallBack

Это больше работает, но работает лучше, поскольку вы не зависите от порядка вызовов:

Рабочий пример:

<?php

class FooTest extends PHPUnit_Framework_TestCase {


    public function testX() {

        $context = $this->getMockBuilder('Context')
           ->getMock();

        $context->expects($this->exactly(2))
           ->method('offsetGet')
           ->with($this->logicalOr(
                     $this->equalTo('Matcher'), 
                     $this->equalTo('Logger')
            ))
           ->will($this->returnCallback(
                function($param) {
                    var_dump(func_get_args());
                    // The first arg will be Matcher or Logger
                    // so something like "return new $param" should work here
                }
           ));

        $context->offsetGet("Matcher");
        $context->offsetGet("Logger");


    }

}

class Context {

    public function offsetGet() { echo "org"; }
}

Это выведет:

/*
$ phpunit footest.php
PHPUnit 3.5.11 by Sebastian Bergmann.

array(1) {
  [0]=>
  string(7) "Matcher"
}
array(1) {
  [0]=>
  string(6) "Logger"
}
.
Time: 0 seconds, Memory: 3.00Mb

OK (1 test, 1 assertion)

Я использовал $this->exactly(2) в матчи, чтобы показать, что это также работает с подсчетом вызовов. Если вам не нужно, чтобы его замена на $this->any(), конечно, работала.

Ответ 3

Вы можете добиться этого с помощью обратного вызова:

class MockTest extends PHPUnit_Framework_TestCase
{
    /**
     * @dataProvider provideExpectedInstance
     */
    public function testMockReturnsInstance($expectedInstance)
    {
        $context = $this->getMock('Context');

        $context->expects($this->any())
           ->method('offsetGet')
           // Accept any of "Matcher" or "Logger" for first argument
           ->with($this->logicalOr(
                $this->equalTo('Matcher'),
                $this->equalTo('Logger')
           ))
           // Return what was passed to offsetGet as a new instance
           ->will($this->returnCallback(
               function($arg1) {
                   return new $arg1;
               }
           ));

       $this->assertInstanceOf(
           $expectedInstance,
           $context->offsetGet($expectedInstance)
       );
    }
    public function provideExpectedInstance()
    {
        return array_chunk(array('Matcher', 'Logger'), 1);
    }
}

Должен пройти для любых аргументов "Logger" или "Matcher", переданных методу Context Mock offsetGet:

F:\Work\code\gordon\sandbox>phpunit NewFileTest.php
PHPUnit 3.5.13 by Sebastian Bergmann.

..

Time: 0 seconds, Memory: 3.25Mb

OK (2 tests, 4 assertions)

Как вы можете видеть, PHPUnit выполнил два теста. Один для каждого значения dataProvider. И в каждом из этих тестов он сделал утверждение для with() и одно для instanceOf, следовательно, четыре утверждения.

Ответ 4

Следуя от ответа @edorian и комментариям (@MarijnHuizendveld) относительно обеспечения того, чтобы метод вызывался как с Matcher, так и с Logger, а не просто дважды с помощью Matcher или Logger, вот пример.

$expectedArguments = array('Matcher', 'Logger');
$context->expects($this->exactly(2))
       ->method('offsetGet')
       ->with($this->logicalOr(
                 $this->equalTo('Matcher'), 
                 $this->equalTo('Logger')
        ))
       ->will($this->returnCallback(
            function($param) use (&$expectedArguments){
                if(($key = array_search($param, $expectedArguments)) !== false) {
                    // remove called argument from list
                    unset($expectedArguments[$key]);
                }
                // The first arg will be Matcher or Logger
                // so something like "return new $param" should work here
            }
       ));

// perform actions...

// check all arguments removed
$this->assertEquals(array(), $expectedArguments, 'Method offsetGet not called with all required arguments');

Это с PHPUnit 3.7.

Если метод, который вы тестируете, фактически ничего не возвращает, и вам просто нужно проверить, что он вызывается с правильными аргументами, применяется тот же подход. Для этого сценария я также попытался сделать это, используя функцию обратного вызова для $this- > callback в качестве аргумента для с, а не returnCallback в завещании. Это терпит неудачу, поскольку внутренне phpunit вызывает обратный вызов дважды в процессе проверки обратного вызова совпадения аргументов. Это означает, что подход завершается неудачно, поскольку во втором вызове этот аргумент уже удален из массива ожидаемых аргументов. Я не знаю, почему phpunit называет это дважды (кажется ненужной тратой), и я думаю, вы могли бы обойти это, удалив его только во втором вызове, но я был недостаточно уверен, что это предполагаемое и последовательное поведение phpunit для полагайтесь на это.

Ответ 6

Мои 2 цента к теме: обратите внимание при использовании в ($ x): это означает, что ожидаемым вызовом метода будет вызов метода ($ x + 1) th на макет объекта; это не означает, что будет ($ x + 1) -й вызов ожидаемого метода. Это заставило меня потратить немного времени, поэтому я надеюсь, что это не с вами. С уважением к каждому.