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


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

$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, и он терпит неудачу, когда я запрашиваю сопоставитель, потому что он ожидает аргумент "Регистратор". Это похоже на то, что второе ожидание переписывает первое, но когда я сбрасываю макет, все выглядит нормально.

Author: Václav Novotný, 2011-03-30

7 answers

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

 28
Author: leeb, 2014-01-28 11:11:03

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

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

Использование ->в($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()));

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

Также это не сработает, если у вас будет более одного вызова каждой из функций!


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

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

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

<?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(), конечно, сработает.

 59
Author: edorian, 2014-09-04 09:26:09

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

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

Должно передаваться для любых аргументов "Регистратор" или "Сопоставитель", переданных методу 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 провел два теста. По одному для каждого значения поставщика данных. И в каждом из этих тестов он сделал утверждение для with() и одно для instanceOf, следовательно, четыре утверждения.

 7
Author: Gordon, 2011-03-30 10:51:48

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

$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->обратный вызов в качестве аргумента для with, а не обратный вызов в завещании. Это не удается, так как внутренне phpunit дважды вызывает обратный вызов в процессе проверки обратного вызова сопоставителя аргументов. Это означает, что подход завершается неудачей, так как при втором вызове этот аргумент уже был удален из массива ожидаемых аргументов. Я не знаю, почему phpunit называет это дважды (кажется ненужной тратой), и я думаю, вы могли бы обойти это, только удалив его при втором вызове, но я не был достаточно уверен, что это намеренное и последовательное поведение phpunit, чтобы полагаться на это.

 5
Author: crysallus, 2014-01-16 03:40:35

Я только что наткнулся на это расширение PHP для макетирования объектов: https://github.com/etsy/phpunit-extensions/wiki/Mock-Object

 2
Author: powtac, 2011-03-30 09:49:36

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

 2
Author: Alessandro Ronchi, 2012-08-13 09:04:55

Вот также некоторые решения с библиотекой doublit:

Решение 1: использование Stubs::returnValueMap

/* Get a dummy double instance  */
$double = Doublit::dummy_instance(Context::class);

/* Test the "offsetGet" method */
$double::_method('offsetGet')
    // Test that the first argument is equal to "Matcher" or "Logger"
    ->args([Constraints::logicalOr('Matcher', 'Logger')])
    // Return "new Matcher()" when first argument is "Matcher"
    // Return "new Logger()" when first argument is "Logger"
    ->stub(Stubs::returnValueMap([['Matcher'], ['Logger']], [new Matcher(), new Logger()]));

Решение 2: использование обратного вызова

/* Get a dummy double instance  */
$double = Doublit::dummy_instance(Context::class);

/* Test the "offsetGet" method */
$double::_method('offsetGet')
    // Test that the first argument is equal to "Matcher" or "Logger"
    ->args([Constraints::logicalOr('Matcher', 'Logger')])
    // Return "new Matcher()" when first argument $arg is "Matcher"
    // Return "new Logger()" when first argument $arg is "Logger"
    ->stub(function($arg){
        if($arg == 'Matcher'){
            return new Matcher();
        } else if($arg == 'Logger'){
            return new Logger();
        }
    });
 0
Author: gealex, 2018-07-26 09:10:34