Тестирование внутренних методов с PHPUnit и runkit

PHPUnit хороший инструмент для тестирования PHP-кода. Но у него есть существенные ограничения влияющие его использование рядовыми программистами. Я даже не говорю об установке. Дело в том что юнит-тесты подразумевают изоляцию методов (у объектов) от внешних зависимостей. Первое с чего начинают объяснение принципов unit-тестов, так это чистенький арифметический метод. Именно поэтому юнит-тесты я до этого использовал только для таких простых функций, которые либо делают финансовые вычисления, либо трансформируют и парсят данные.

Нельзя не согласиться, что такой подход вынуждает писать простой для понимания код с минимумом зависимостей. Но когда дело касается настоящих методов, то PHPUnit опускает руки - он не умеет изолировать внутренние методы! И до каких-то пор не умел статические и приватные/защищённые тестировать тоже. Вот что я имею ввиду..

public function methodIWantToTest(){ $s=''; for($i=1;$i<10;$i++){ $s.=$this->innerMethodIWantToAvoidCalling($i); } return $s; }

Это серъёзная проблема, ведь редко какой сложный метод обходится без вызова внутренних. Один из выходов — использовать новый instance этого же класса, и с помощью моков сымитировать поведение внутренней переменной (фактически запрещая использования $this). Моки конечно полезные, но я нехочу из-за ограничений фреймворка для тестирования менять свой код в неоптимальную сторону, добавляя выделение памяти под объект который уже и так создан. Поэтому..

Runkit

runkit в phpinfo()Runkit это экспериментальное расширение для php, позволяющее перезаписывать структуру классов на лету, что собственно нам и надо, потому что стандартный Reflection просто создаёт новые классы, а нам надо совместить существующий код с перезаписью внутреннего метода сделав из него заглушку (stub).

Самое неприятное тут это установка, потому что обычный pecl-install не работает и приходится делать checkout из репозитория с локальной установкой, потом копировать файл расширения (so/dll), прописывать в php.ini. Ну и потом у меня были опять проблемы из-за того что я использую не стандартный php на маке, а тот что в MAMP.

В итоге, после того как я инициализировал объект от класса, я просто перезаписываю его внутреннюю функцию на постоянно возвращаемое значение которое мне надо:

runkit_method_redefine('MyClassName','innerMethodIWantToAvoidCalling','','return "someFixedValue";',RUNKIT_ACC_PUBLIC);

Это конечно не так хорошо как динамический мок, но лучше чем не тестировать методы высокого уровня вообще

Unit-тестирование в Kohana 3.2

Kohana - один из десятка php-фреймворков, имеющий в том числе и модуль для юнит-тестирования написанного кода. Про тестирование в общем я уже писал, про настройку phpunit тоже. Упростив статью одного безымянного товарища, в сторону использования PHPStorm IDE вот к чему я пришёл..

  1. В application/boostrap.php надо найти строчку с unittest модулем и раскоментировать
  2. Создать папку и файл application/tests/boostrap.php - тут у нас будут лежать тесты для приложения
    define('SUPPRESS_REQUEST', true); require_once('../../index.php');
  3. Обернуть последние строчки index.php в IF что-бы для юнит-тестов не выводилась заглавная страница if (!defined('SUPPRESS_REQUEST')){ echo Request::factory()... }
  4. Теперь в настройках PHPStorm 3.0 (Run-Edit configurations) прописываем пути с запуском всей папки.. Настройки тестирования с PHPUnit в PHPStorm с путями к Kohana 3.2 на MacOS X Lion

Теперь можно добавлять тесты в application/tests/classes/ где уже будет работать автозагрузка Kohana-классов. В моём примере тестируется класс Model_Message который отвечает за форматирование текста для оповещения пользователя. То как хранятся оповещения или переводятся в данном случае неважно - тестируется конкретный случай передачи массива и получения строки.

defined('SYSPATH') or die('No direct access allowed!'); /** @property Model_Message object */ class MessageTest extends Unittest_TestCase { private $object; public function setUp() { parent::setUp(); $this->object = new Model_Message(); } /** @test */ public function formatFriendMessageWeb() { $result = $this->object->formatForWeb(array( 'object'=> 'friend', 'action'=> 'request' )); $this->assertEquals($result, 'asked to friend'); } }

Сам тестируемый объект я не привожу, тут важно как идёт подгрузка, запуск и результат:

Результат тестирования в PHPStorm 3

Анализ ошибок с XDebug и PHPStorm 2.0

XDebug это отличный php-модуль для правильного дебага приложения, который в «старших» языках (читай - не интерпретируемых) уже сразу был встроен в компилятор. Необходимость в полноценном дебаге очевидная в сложных приложениях, где воспроизведение ошибки занимает относительно много времени, а объём данных не позволяет копаться в мегабайтах от print_r(), хотя этот модуль позволяет и такие отчётыОбычный stack trace в браузере

Посколько xdebug это модуль, то не на всяком shared-хостинге он имеется и поэтому подразумевается что разработчик подымет у себя php+xdebug сам. После этого в php.ini включается модуль и его настройки по умолчанию. Заметьте что remote_host по IP ограничивает число работающих с дебагерром.

extension=C:\Program Files\php\ext\php_xdebug-2.1.0-5.3-vc9.dll
xdebug.profiler_enable = 1
xdebug.remote_host=127.0.0.1
xdebug.remote_port=9000
xdebug.remote_handler=dbgp
xdebug.idekey=

Для правильного дебага надо соединить IDE и модуль Xdebug, что-бы последний остановил работу php и передал данные всех переменных в PHPStorm:

  1. Включить прослушивание 9000 порта в PHPStorm и режим дебага в IDE через Shift+F9 или из меню Run/Debug.
    listen_off.png

  2. Прописать в куки режим дебага что-бы X-debug на удалённом сервере понял когда ему стоит работать. Jetbrains сделали генератор букмарков для простой работой с печеньками. Второй вариант - специальные плагины для браузеров. Параметр Ide key вводится такой же как и в настройках IDE и в php.ini


Теперь можно поставить breakpoint на любой строке. Проблемы начинаются когда данные таки начинают бегать -  если удалённый сервер на линуксе, а у вас винда то естественно пути которые на линуксе не совсем соответсвуют той иерархии кода которую видет PHPStorm. Благо эта проблема решается маппингом папок на нужное место. Вторая проблема - закрытый код. XDebug как ни в чём не бывало выдаёт все пути что запускалось.. а IDE ведь не может этого проверить.

Пример дебага без break point

Наконец главная проблема - как выкидывать trace автоматически при возникновении ошибки, без установки breakpoint? Я частично решил для себя эту проблему используя свой регистратор ошибок вместе с xdebug_break(), проблема в том что фатальные ошибки не до конца показывают stack trace.

function ErrorHandler($errno, $errstr, $errfile, $errline) { if (!in_array($errno,array(E_NOTICE,2048))) { xdebug_break(); restore_error_handler(); trigger_error($errno.$errstr." in ".$errfile." on line ".$errline."; showed by error handler "); } } function shutDownFunction() { if(!is_null($e = error_get_last())) xdebug_break();} function exceptionHandler($exception) { xdebug_break(); restore_exception_handler();} set_error_handler('ErrorHandler'); register_shutdown_function('shutdownFunction'); set_exception_handler('exceptionHandler');

FastCGI process manager для PHP

Набрёл на интересную презентацию Андрея Нигматулина на phpconf08, которій рассказывает о проблемах PHP на высоких нагрузках. О apache, nginx, eaccelerator, fastcgi и своём менеджере php-процессов.

BRMS на php с отражениями

Отражения (Reflection API) в php — мощный инструмент для самоанализа кода. Давно не писал ничего интересного, а тут такая интересная мини-задачка - написать маленькую систему бизнес-правил aka BRMS для обработки сложных форм, причём не просто десять табов который сохраняются в БД, а анализ который приводит к каким-то выводам.

В качестве ядерного решения  выступает вызов правил как методов, но тут ещё такая особенность что поскольку форма не одна, и поскольку они очень похожи, то решение - вызывать методы на основе входных данных. Грубо говоря - приходит 40 input-полей, мы анализируем какие из этих полей подходят в качестве аргументов конкретному методу (скажем 3) и вызываем его уже с 3 аргументами (вместо передачи всего массива).

Как я выше написал, анализ проводится с помощью малодокументированными но вполне рабочими отражениями. В итоге примерно такой код..
$oRuleContainer = new cRuleContainer(); //просто класс с методами-правилами $rContainer = new ReflectionClass('cRuleContainer'); //отражение класса //где-то тут цикл по вызываемым методам, можно проходится по всем //но я проходил по методам из базы, поэтому его опускаю.. тут появляется $aRule $rMethod = $rContainer->getMethod($aRule['method']); $aArgs = $rMethod->getParameters(); //выбираем только нужные аргументы if($aArgs){ foreach($aArgs as $refArgument){ $arrPassedArgData[$refArgument->name]=$_POST[$refArgument->name]; } } if(call_user_func_array(array($oRuleContainer,$aRule['method']),$arrPassedArgData)){ //правило сработало }
Кстати, я прекрасно понимаю что можно вызывать правила без ничего, читая всё из POST, но тут решение эстетическое и повторно используемое.