The trick is quite simple, once you know it. When you are in namespaced code and call a function, PHP will first search your namespace for that function, and then fall back to the global namespace, if it doesn’t find a match.
Prerequisites ¶
So for this to work, you need these two aspects implemented in your code:
- Use a namespace, and
- do not call global functions explicitly, that is, not like
\strlen($foo)
.
Then you can leverage the fallback behaviour to your advantage: In a
bootstrap file for your unit tests define functions in your namespace, that
match the ones you want to overwrite. Then return the desired value from them
to be used in your unit tests. The following bootstrapping code is sufficient
to shadow session_status
:
<?php
namespace MyNamespace;
/* always claims an active session */
function session_status() {
return PHP_SESSION_ACTIVE;
}
In your code, when calling session_status()
, it will then
always state an active session:
<?php
namespace MyNamespace;
/* calls our mock function */
$session_is_active = session_status();
/* don't do this, though! */
$session_is_active = \session_status();
(Note, that your code and the mocking function need to be in the same namespace!)
Refinement ¶
The simple approach has a couple shortcomings. For one, the mocked function cannot
decide, from where it was called. It may well be, that you need true
in ine test, but false
in another.
debug_backtrace
comes to the rescue! We can simply loop through the stack and see, where the mocking
function was called:
<?php
namespace MyNamespace;
/* sometimes claims an active session */
function session_status() {
$backtrace = debug_backtrace();
foreach ($backtrace as $step) {
if ($step['function'] === 'needInactiveSession') {
return PHP_SESSION_NONE;
}
}
return PHP_SESSION_ACTIVE;
}
When one of the functions/methods in the stack is named
needInactiveSession
, the mock now returns an inactive session.
Then we might, for some reasons, return the original function’s value. This is
achieved straight-forward. The combo
call_user_func_array
and
func_get_args
allows this to be extremely
flexible for all cases.
<?php
namespace MyNamespace;
/* sometimes claims an active session */
function session_status() {
return call_user_func_array('\\session_status', func_get_args());
}
Third, the bootstrapping code might accidentally be loaded in a production environment. Do we have a possibility to narrow or even remedy the impact of such an error? Yes. With both previous methods combined, we can scan the calling stack for trigger functions and classes, like unit test class names, and return mocked values only then. In all other cases we return the value of the PHP built-in. Adequate naming assumed this makes it completely safe to mock built-in functions, even when the mocks end up in the call path.
And last but not least, you will find yourself writing very much mocking boilerplate in this way. Wouldn’t it be nice to abstract that away in a single function? The code should look like this:
<?php
namespace MyNamespace;
function session_status() {
return _mock(array(
'called_from_this_function' => PHP_SESSION_DISABLED,
'CalledFromAnyMethodInThisClass' => PHP_SESSION_ACTIVE,
'OtherClass::specificMethod' => PHP_SESSION_NONE,
/* any stacktrace with none of the above should return the original
* value of \session_status() */
));
}
Implementation ¶
Here is a complete function for mocking functions, that implements the above requirements:
<?php
namespace MyNamespace;
/**
* mock return values for built-in functions in unit tests
*
* Usage: Create a function MyNamespace\php_built_in to shadow
* a PHP built-in php_built_in(). Define its mock behaviour in terms
* of calling stack. If any of the calling functions/methods are detected,
* return the given value:
*
* function php_built_in() {
* return _mock(array(
* 'a_function' => 42,
* 'MyClass' => true,
* 'OtherClass::otherMethod' => 'foobar',
* ));
* }
*
* This way, calling php_built_in will either return `42` when the function
* `a_function` is involved, `true` for calls where any method of `MyClass`
* occurs in the stack, `foobar` for `OtherClass::otherMethod` and a true call
* to php_built_in if neither is in the stack.
*
* You can also use _mock to just fiddle with the arguments but still return
* the original function:
*
* function php_built_in() {
* $args = func_get_args();
* // change some arguments in $args and then...
* return _mock(array(), $args);
* }
*
*/
function _mock($when=array(), $args=null) {
$backtrace = debug_backtrace();
array_shift($backtrace); // self
$latest = array_shift($backtrace); // calling function to be mocked
$function = str_replace(__NAMESPACE__.'\\', '', $latest['function']);
/* you can provide your own arguments. If not, take the ones from the
* backtrace */
if ($args === null) {
$args = $latest['args'];
}
/* loop through invoking methods/functions. If one matches a key in
* $when, return $when's value */
foreach ($when as $invoke => $value) {
foreach ($backtrace as $step) {
if (
/* "MyClass" or "MyClass::myMethod" */
(array_key_exists('class', $step) &&
($step['class'] === $invoke ||
$step['class'].'::'.$step['function'] === $invoke)) ||
/* "my_function" */
(! array_key_exists('class', $step) &&
$step['function'] === $invoke)
) {
return $value;
}
}
}
/* return PHP's built-in function call */
return call_user_func_array("\\$function", $args);
}
(Licensing trifles: You can use above code under both GPL and MIT licenses. Choose at your liking.)