Функция PHPUnit mock?
У меня есть интересный сценарий в том, что мне нужна функция, которая должна быть определена для проведения тестов для другой функции. Функция, которую я хочу протестировать, выглядит примерно так:
if (function_exists('foo') && ! function_exists('baz')) {
/**
* Baz function
*
* @param integer $n
* @return integer
*/
function baz($n)
{
return foo() + $n;
}
}
Причина, по которой я проверяю существование foo
, заключается в том, что она может быть определена или не определена в проекте разработчика, а функция baz
основана на foo
. Из-за этого я хочу, чтобы baz
определялся, если он может вызвать foo
.
Единственная проблема заключается в том, что до сих пор было невозможно писать тесты. Я попытался создать загрузчик script в конфигурации PHPUnit, который определит функцию fake foo
, а затем потребует автозагрузчик Composer, но мой основной script по-прежнему считает, что foo
не определен. foo
не является пакетом Composer и в противном случае не может потребоваться моему проекту. Очевидно, Mockery тоже не сработает. Мой вопрос: если кто-то более опытный с PHPUnit столкнулся с этой проблемой и нашел решение.
Спасибо!
Ответы
Ответ 1
Это должно сработать!
use MyProject\baz;
class YourTestCase
{
/** @var callable **/
protected $mockFoo;
/** @var callable **/
protected $fakeFoo;
public function setUp()
{
if (function_exists('foo')) {
$this->mockFoo = function($foosParams) {
foo($foosParams);
// Extra Stuff, as needed to make the test function right.
};
}
$this->fakeFoo = function($foosParams) {
// Completely mock out foo.
};
}
public function testBazWithRealFoo()
{
if (!$this->mockFoo) {
$this->markTestIncomplete('This system does not have the "\Foo" function.');
}
$actualResults = baz($n, $this->mockFoo);
$this->assertEquals('...', $actualResults);
}
public function testBazWithMyFoo()
{
$actualResults = baz($n, $this->fakeFoo);
$this->assertEquals('...', $actualResults);
}
}
Затем измените существующий код:
if (function_exists('foo') && ! function_exists('baz')) {
/**
* Baz function
*
* @param integer $n
* @return integer
*/
function baz($n)
{
return foo() + $n;
}
}
namespace MyProject
{
function baz($bazParams, callable $foo = '\foo')
{
return $foo() + $bazParams;
}
}
Затем вместо вызова baz($n)
необходимо вызвать:
use MyProject\baz;
baz($bazParams);
Он похож на Injection Dependency для функций, yo; -)
Ответ 2
Кажется, это ситуация, когда вам нужно притворяться видимостью, которая определенно выходит за рамки PHPUnit, которая представляет собой библиотеку классов. Таким образом, вы все равно можете использовать PHPUnit, но, возможно, придется выйти за его пределы, чтобы достичь своей цели.
Единственный способ, с помощью которого я могу создать макет функции, находится прямо внутри тестового примера, над объектом, расширяющим \PHPUnit_Framework_TestCase.
function foo() {
return 1;
}
if (function_exists('foo') && ! function_exists('baz')) {
/**
* Baz function
*
* @param integer $n
* @return integer
*/
function baz($n)
{
return foo() + $n;
}
}
class TestBaz extends \PHPUnit_Framework_TestCase
{
public function test_it() {
$this->assertSame(4,baz(3));
}
}
Возможно, вам придется создать два файла: один с foo и один без, или четыре, если вы хотите протестировать foo/not foo и baz/not baz. Это определенно внешний вариант, который я бы обычно не рекомендовал, но в вашем случае это может быть вашим лучшим выбором.
Ответ 3
Начните с небольшого рефакторинга кода, чтобы сделать его более проверяемым.
function conditionalDefine($baseFunctionName, $defineFunctionName)
{
if(function_exists($baseFunctionName) && ! function_exists($defineFunctionName))
{
eval("function $defineFunctionName(\$n) { return $baseFunctionName() + \$n; }");
}
}
Затем просто назовите его следующим образом:
conditionalDefine('foo', 'bar');
Ваш тестовый класс PHPUnit будет содержать следующие тесты:
public function testFunctionIsDefined()
{
$baseName = $this->mockBaseFunction(3);
$expectedName = uniqid('testDefinedFunc');
conditionalDefine($baseName, $expectedName);
$this->assertTrue(function_exists($expectedName));
$this->assertEquals(5, $expectedName(2));
}
public function testFunctionIsNotDefinedBecauseItExists()
{
$baseName = $this->mockBaseFunction(3);
$expectedName = $this->mockBaseFunction($value = 'predefined');
conditionalDefine($base, $expectedName);
// these are optional, you can't override a func in PHP
// so all that is necessary is a call to conditionalDefine and if it doesn't
// error, you're in the clear
$this->assertTrue(function_exists($expectedName));
$this->assertEquals($value, $expectedName());
}
public function testFunctionIsNotDefinedBecauseBaseFunctionDoesNotExists()
{
$baseName = uniqid('testBaseFunc');
$expectedName = uniqid('testDefinedFunc');
conditionalDefine($base, $expectedName);
$this->assertFalse(function_exists($expectedName));
}
protected function mockBaseFunction($returnValue)
{
$name = uniqid('testBaseFunc');
eval("function $name() { return '$value'; }");
return $name;
}
Этого достаточно для того, что вы просите. Тем не менее, вы можете реорганизовать этот код, извлекая генерацию функции в генератор кода. Это то, что вы можете написать модульные тесты против генератора, чтобы убедиться, что он создает такую функцию, которую вы ожидаете.
Ответ 4
Достаточно ли этого?
к test.php:
<?php
if (function_exists('foo') && ! function_exists('baz')) {
/**
* Baz function
*
* @param integer $n
* @return integer
*/
function baz($n)
{
return foo() + $n;
}
}
BazTest.php:
<?php
class BazTest extends PHPUnit_Framework_TestCase {
public function setUp()
{
function foo()
{
// Appropriate mock goes here
return 1;
}
include __DIR__ . '/to-test.php';
}
public function testBaz()
{
$this->assertEquals(2, baz(1));
$this->assertEquals(3, baz(2));
}
}
Запуск теста дает:
PHPUnit 5.4.8 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 54 ms, Memory: 8.00MB
OK (1 test, 2 assertions)
Ответ 5
Добавление его в файл начальной загрузки не работает - почему?
Это потому, что иногда функция baz
является (A) правильно созданной системой, а иногда (B) вам нужно ее издеваться? Или (C) вам всегда нужно издеваться над этим?
- Случай A: Почему код, создающий жизненно важную функцию спорадически на лету?
- Случай B: Функция может регистрироваться только один раз и никогда не регистрироваться или перезаписываться. Поэтому вы либо идете с макетом, либо нет. Смешивание не допускается.
- Случай C: Если вам всегда нужно высмеять его, и вы добавите его в файл начальной загрузки, он будет определен. Что касается того, что вы пробовали, либо ваш загрузочный файл для phpunit загружен неправильно, либо вы ошибочно написали имя функции.
Я уверен, что вы правильно настроили загрузку phpunit, но в хорошем смысле это выглядит примерно так:
/tests/phpunit.xml
:
<phpunit
bootstrap="phpunit.bootstrap.php"
</phpunit>
/tests/phpunit.bootstrap.php
:
<?php
require(__DIR__ . "/../bootstrap.php"); // Application startup logic; this is where the function "baz" gets defined, if it exists
if (function_exists('foo') && ! function_exists('baz')) {
/**
* Baz function
*
* @param integer $n
* @return integer
*/
function baz($n)
{
return foo() + $n;
}
}
Не создавайте функцию baz
на лету в ваших тестах, например. в функции setUp
.
В тестовых наборах в phpunit используется один и тот же загрузочный файл. Поэтому, если вам нужно проверить случаи, когда определена функция baz
, а в других случаях, когда она не определена (и вам нужно ее издеваться), вам нужно разделить папку tests
, например. в двух разных папках, каждый из которых имеет файлы phpunit.xml
и phpunit.bootstrap.php
. Например. /tests/with-baz
и /tests/mock-baz
. Из этих двух папок выполните тесты отдельно. Просто создайте символические ссылки на phpunit
в каждой подпапке (например, из /test/with-baz
create ln -s ../../vendor/bin/phpunit
, если композитор находится в корневом каталоге), чтобы гарантировать, что вы запускаете ту же версию phpunit в обоих сценариях.
Конечным решением является, разумеется, выяснить, где определена функция baz
и вручную включить в нее файл script, если это вообще возможно, для обеспечения правильной логики.
Alternative
Используйте аннотацию phpunit @runInSeparateProcess
и определите функцию по мере необходимости.
<?php
class SomeTest extends \PHPUnit_Framework_TestCase
{
/**
* @runInSeparateProcess
*/
public function testOne()
{
if (false === function_exists('baz')) {
function baz() {
return 42;
}
}
$this->assertSame(42, baz());
}
public function testTwo()
{
$this->assertFalse(function_exists('baz'));
}
}