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