whitewashing.de

Test your Legacy PHP Application with Function Mocks!

27. March 2009

Much talking is going on about Unittesting, Mocks and TDD in the PHP world. For the most this discussions surround object-oriented PHP code, frameworks and applications.

Yet I would assert that the reality for PHP developers (me included) is dealing with PHP 4, PHP 5 migrated, or non-object oriented legacy applications which are near to impossible to bring under test. Still this code works, is in production and needs maintenance and possibly extension for new features.

For example many applications may still use mysql_* functions at length inside their model or have multiple nested function levels (Example: Wordpress). Runkit to the rescue: A PECL extension that can hack your running PHP code, such that method or functions are repointed to execute new implementations. Using this extension you can actually mock out internal PHP functions, which is great to bring legacy code under test.

Consider this following proof of concept, using the Runkit extension build by Padraic Brady (the one from pecl.php.net does not compile on PHP 5.2), which replaces the functionality of mysql_query(). You have to set the following option in your php.ini: runkit.internal_override = On for this to work. By default only user-defined functions may be overwritten by Runkit.

class FunctionMocker
{
    protected $_mockedFuncBehaviourMap = array();

    public function mock($funcName, $return=null)
    {
        $newFuncCode = 'return "'.$return.'";';
    
        $renamedName = "__".$funcName."_mockOriginalCopy";
        runkit_function_copy($funcName, $renamedName);
        runkit_function_redefine($funcName, '', $newFuncCode);
        
        $this->_mockedFuncBehaviourMap[$funcName] = $renamedName;
    }
    
    public function reset()
    {
        foreach($this->_mockedFuncBehaviourMap AS $funcName => $renamedName) {
            runkit_function_remove($funcName);
            runkit_function_copy($renamedName, $funcName);
            runkit_function_remove($renamedName);
        }
        $this->_mockedFuncBehaviourMap = array();
    }
}

$mocker = new FunctionMocker();
$mocker->mock('mysql_query', 'hello world!');

echo mysql_query(); // hello world

$mocker->reset();

mysql_query(); // error

This example, only allows for string return values of the mock and has no support for replacing the arguments of the mocked function. Also chaining of different return values based on input or call number might be interesting. Some kind of Code Generator Tool would have to be implemented to support this functionality. Additionally Assertions and Verifying should be implemented for the function arguments. All in all this would allow to mock functions as you would mock interfaces/classes, which would be a great addition for all those legacy applications that use procedural PHP.

Additionally what the real killer for runkit would be: The possibility to insert PHP callbacks instead of real PHP code into the runkit_function_redefine.

:: Category: PHP, Comments (4)

Tags for this Article

Comments (4)

Pretty slick. I thought about using runkit for mocking purposes when dealing with hard to test stuff like header() etc. pp. Pretty cool you did it. Any measurements on how runkit makes test execution slower because of function overwrite?
Btw: allowing callbacks shouldn't be hard to do in the. Just check if the passed argument is callable (zend_is_callable()), if it is, put the callback into runkits globals under the target name of the function (use RUNKIT_G() or how it is named there to access TSRMG), and register your own function pointer which calls the callback from globals by looking up the function name in TSRMG and calling it with call_user_function(). User: lars strojny - Date: 2009/03/27 22:57
though I haven't actually tried it, there is a function mocking library, that uses runkit to mock out functions, and might be worth to check out: http://code.google.com/p/php-mock-function/ User: fqqdk - Date: 2009/03/31 20:27
I wrote a library that we are using in a test framework at work. In the process I did find a few bugs with runkit that have been committed to CVS at php.net but no release of runkit has been done in a while. So this does work with CVS copy of runkit:

http://c01.ds-o.com/svn/mockfunction/trunk/ User: mike.lively - Date: 2009/04/02 05:28
I am in need of just such a mock-function-utility. So I've just pushed a fork of runkit into github that allows one to attach intercepting callbacks to functions. It basically works as Lars Strojny describes above.

The usual disclamers ofcourse, it's just a trial. User: teddy grenman - Date: 2009/06/25 22:25

Post A Comment

 
 
 
 
::Back To The Top