I have worked on many different PHP-based projects and sometimes I need to modify old and not clean codes. There I have faced untested codes. However, not just there could need to mock global functions. Sometimes we need that for PHP built-in functions also.
The code
For example, you have a similar class:
src/DateGetter.php
<?php
namespace MyNamespace;
class DateGetter
{
public function get_date($format = 'Y-m-d H:i:s', $timestamp = null)
{
if (is_null($timestamp)) {
$timestamp = time();
}
return date($format, $timestamp);
}
}
As you can see, there are two PHP built-in functions: time()
and date()
.
The basic concept
From version 5.3 in PHP we have namespaces. We can use namespaces to organize our code. And we can use this possibility to test global functions. We can only test global function calls when it called in a namespace.
When we call a function inside a namespace, PHP will search for the function in the current namespace. If it does not find it, it will search for it in the global namespace. We can use this to our advantage.
We can override the global function in the current namespace. We can do this with the namespace
keyword. We can replace the global function with the same name in the current namespace.
If we call the time()
in the MyNamespace
namespace, PHP will call the time()
function in the MyNamespace
namespace. So we can override the global function with the same name in the MyNamespace
namespace.
<?php
namespace MyNamespace;
function time()
{
return 1234567890;
}
Go advanced
I would like to search for a solution where I can set up the global function with the PHPUnit to make it more flexible and the calls can be testable.
I have found a solution. I have created helper classes in the same namespace.
For the full view, I will show you how I override the global functions in the MyNamespace
namespace.
<?php
namespace MyNamespace;
function time() {
return call_user_func([GlobalFunctionsMocker::class, 'time']);
}
function date(...$args) {
return call_user_func_array([GlobalFunctionsMocker::class, 'date'], $args);
}
- The Global Functions Mocker class
The goal of this class is to provide the possibility to be callable from anywhere and forward the calls to the PHPUnit mock object.
<?php
namespace MyNamespace;
class GlobalFunctionsMocker
{
/** @var GlobalFunctionsMockPlaceholder */
static $mock;
public static function time()
{
return call_user_func([self::$mock, 'time']);
}
public static function date(...$args)
{
return call_user_func_array([self::$mock, 'date'], $args);
}
}
- The Global Functions Helper class
The goal of this class is to create a very simple class with the functions that we would like to mock. We can use this class to mock the global functions. I would like to use the mock of this class to set it to the GlobalFunctionsMocker::$mock
property.
<?php
namespace MyNamespace;
class GlobalFunctionsMockPlaceholder
{
public function time()
{
}
public function date()
{
}
}
In this way, we can mock the global functions with PHPUnit. The call stack seems like this for example:
- Call the
time()
function in theMyNamespace
namespace. - Call the
GlobalFunctionsMocker::time()
function. - Call the
GlobalFunctionsMocker::$mock->time()
function. So thetime()
function of the mockedGlobalFunctionsMockPlaceholder
class.
The test
The environment is ready. We can write the test. We can create a mock for global functions and to use that, we can set it to the GlobalFunctionsMocker::$mock
property.
<?php
namespace MyNamespace;
class DateGetterTest extends PHPUnit_Framework_TestCase
{
public function testGetDate()
{
$GlobalFunctionMock = $this->createMock(GlobalFunctionMockPlaceholder::class);
$GlobalFunctionMock->expects($this->once())
->method('time')
->willReturn(1420070400);
$GlobalFunctionMock->expects($this->once())
->method('date')
->with('Y-m-d H:i:s', 1420070400)
->willReturn('2015-01-01 00:00:00');
GlobalFunctionsMocker::$mock = $GlobalFunctionMock;
$this->assertEquals(
'2015-01-01 00:00:00',
(new DateGetter())->get_date()
);
}
}
Summary
I hope it helps to understand the possibilities behind the namespaces with global functions. I hope it helps to test your legacy code. If you want to try out the concept please see the code in my repository with the example.