Макет в 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, и он терпит неудачу, когда я запрашиваю сопоставитель, потому что он ожидает аргумент "Регистратор". Это похоже на то, что второе ожидание переписывает первое, но когда я сбрасываю макет, все выглядит нормально.
7 answers
Начиная с PHPUnit 3.6, существует $this->returnValueMap()
который может использоваться для возврата различных значений в зависимости от заданных параметров в заглушку метода.
К сожалению, это невозможно с помощью 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()
, конечно, сработает.
Вы можете добиться этого с помощью обратного вызова:
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
, следовательно, четыре утверждения.
Следуя ответу @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, чтобы полагаться на это.
Я только что наткнулся на это расширение PHP для макетирования объектов: https://github.com/etsy/phpunit-extensions/wiki/Mock-Object
Мои 2 цента в тему: обратите внимание при использовании at($x): это означает, что ожидаемый вызов метода будет ($x+1)-м вызовом метода для макетного объекта; это не значит, что это будет ($x+1)-й вызов ожидаемого метода. Это заставило меня потратить некоторое время впустую, так что я надеюсь, что с тобой этого не произойдет. С наилучшими пожеланиями всем.
Вот также некоторые решения с библиотекой 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();
}
});