Макет в PHPUnit - множественная настройка одного и того же метода с разными аргументами - PullRequest
49 голосов
/ 30 марта 2011

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

Ответы [ 7 ]

60 голосов
/ 30 марта 2011

К сожалению, это невозможно при использовании стандартного 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()));

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

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


Принятие обоих параметров и использование 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(), конечно, сработает.

30 голосов
/ 02 апреля 2013

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

7 голосов
/ 30 марта 2011

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

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", передаваемых методу offsetGet Context Mock:

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, следовательно, четыре утверждения.

5 голосов
/ 16 января 2014

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

3 голосов
/ 13 августа 2012

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

2 голосов
/ 30 марта 2011

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

0 голосов
/ 26 июля 2018

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

Решение 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();
        }
    });
...