Давно хотел поделиться с общественностью способом тестировать код, использующий функции для работы с внешней средой: с сокетами, БД, файлами и чем угодно ещё. Сегодня, увидев статью Runkit + PHPUnit = 100% покрытие тестами, решил, что сейчас самое время.
Решение с Runkit красивое, но есть одна проблема — Runkit не распространяется вместе PHP, его надо ставить отдельно. Я же хочу предложить подход, работающий в обычной поставке PHP 5.3+, при одном условии — проект должен использовать пространства имён.
Идея очень проста и основывается на правилах разрешения имён в PHP: вызванная функция сначала ищется в текущем пространстве имён, и только потом вызывается встроенная функция PHP. Например, в следующем куске кода:
PHP сначала попытается вызывать A\foo() и только потом foo(). Значит, если мы объявим функцию с именем, скажем, «fsockopen» в пространстве имён проекта, то вначале вызвана будет именно она.
Предположим, у нас есть вот такой класс:
src/MyClass.php
Чтобы перекрыть функцию fsockopen, создаём в тестах такой вот файл:
tests/helpers.php
Теперь можно написать такой тест:
tests/MyClass_Test.php
В этом тесте будет вызвана не встроенная fsockopen, а объявленная в helpers.php.
Конечно же вариант с $GLOBALS не единственный возможный, и показан здесь только для простоты. То, что предлагаю — это не готовое решение, а лишь подход, который каждый может приспособить под свои нужды.
Решение с Runkit красивое, но есть одна проблема — Runkit не распространяется вместе PHP, его надо ставить отдельно. Я же хочу предложить подход, работающий в обычной поставке PHP 5.3+, при одном условии — проект должен использовать пространства имён.
Идея
Идея очень проста и основывается на правилах разрешения имён в PHP: вызванная функция сначала ищется в текущем пространстве имён, и только потом вызывается встроенная функция PHP. Например, в следующем куске кода:
namespace A;
foo();
PHP сначала попытается вызывать A\foo() и только потом foo(). Значит, если мы объявим функцию с именем, скажем, «fsockopen» в пространстве имён проекта, то вначале вызвана будет именно она.
Применение
Предположим, у нас есть вот такой класс:
src/MyClass.php
<?php
namespace MyNS;
class MyClass
{
public function someMethod()
{
$fp = fsockopen('example.org', 123);
}
}
* This source code was highlighted with Source Code Highlighter.
Чтобы перекрыть функцию fsockopen, создаём в тестах такой вот файл:
tests/helpers.php
<?php
namespace MyNS;
function fsockopen($hostname, $port)
{
$GLOBALS['fsockopen'] = array($hostname, $port);
}
* This source code was highlighted with Source Code Highlighter.
Теперь можно написать такой тест:
tests/MyClass_Test.php
<?php
namespace MyNS\Tests;
use PHPUnit_Framework_TestCase;
use MyNS\MyClass;
require 'helpers.php';
require '../src/MyClass.php';
class MyClass_Test extends PHPUnit_Framework_TestCase
{
public function test_someMethod()
{
$test = new MyClass();
$test->someMethod();
$this->assertEquals(array('example.org', 123), $GLOBALS['fsockopen']);
}
}
* This source code was highlighted with Source Code Highlighter.
В этом тесте будет вызвана не встроенная fsockopen, а объявленная в helpers.php.
Конечно же вариант с $GLOBALS не единственный возможный, и показан здесь только для простоты. То, что предлагаю — это не готовое решение, а лишь подход, который каждый может приспособить под свои нужды.