Goal
Mock functions that have already “loaded” into PHP even before loading Composer Autoloader, any include or other function name(){}
declarations
Mocking not only under a non-empty namespace, for example App\Service\name
, but also from the root namespace: the easiest way to do this is through use function name;
Problem
If we declare a function with a name that already exists in the PHP standard library, we will get an error that such a function already exists and cannot be overridden.
It would be possible to “unload” it from memory, but functions cannot be unloaded from the memory in PHP.
You can only redefine a function before it is directly declared. But this method is not suitable, because the function is already declared before any call php
.
Research
In php.ini you can find the disable_functions
flag, which accepts a list of function names that should not be declared in PHP at startup.
If you use this flag, then php -ddisable_functions=time -r "echo time();"
will throw an error:
❯ php -ddisable_functions=time -r "echo time();"
PHP Fatal error: Uncaught Error: Call to undefined function time() in Command line code:1
Stack trace:
#0 {main}
thrown in Command line code on line 1
Fatal error: Uncaught Error: Call to undefined function time() in Command line code on line 1
Error: Call to undefined function time() in Command line code on line 1
Call Stack:
0.0000 389568 1. {main}() Command line code:0
So logical. The time
function no longer exists. But now you can create it yourself?
If you declare the function by yourself, there will no errors:
❯ php -ddisable_functions=time -r "function time() { return 123; } echo time();"
123%
Bingo!
We place the function declaration in the library, create a State manager, through which we can manage the return value “123” and create an interface for the user to interact with this manager.
Now, if a user wants to test the time
call, we can independently specify the required values. Time in the future, in the past, 0, false
, whatever.
But what if you need to test the modified time
function only one time, and leave everything as is in other places?
You can do this: State manager creates a function for all tests that emulates the standard time
, and in the desired test, overlay a private one on top of the general emulation.
Everything seems logical and understandable. You can code and enjoy testing.
However, how to emulate system time? If everything is clear with various polyfills from symfony: you can create some kind of function that will be based on another function, convert the result to a new format and return it.
But on what function should time be based?
DateTime
* classes? date()
? mktime
? hrtime
? What if you need to turn them off too?
Bash! 🤪
PHP has an ability to refer to its big brother Bash at any time with simple backticks: command
. The result will be a string, but you can always cast it.
For the analogue of time()
the command is date +%s
.
This means that the only thing left for the State manager to write is the ability to use not a static value, but a function that will be executed every time.
All this and more is done in the library xepozz/internal-mocker
We read the installation and initial setup document, add the necessary files, enter the following configuration:
$mocker = new Mocker();
$mocker->load([
[
'namespace' => '',
'name' => 'time',
'function' => fn() => `date +%s`,
],
]);
MockerState::saveState();
And we get a generated mock for the time
function, which will simply always work like a regular time
in PHP itself
namespace {
use Xepozz\InternalMocker\MockerState;
function time(...$arguments)
{
if (MockerState::checkCondition(__NAMESPACE__, "time", $arguments)) {
return MockerState::getResult(__NAMESPACE__, "time", $arguments);
}
return MockerState::getDefaultResult(__NAMESPACE__, "time", fn () => `date +%s`);
}
}
And it’s very easy to test:
namespace Xepozz\InternalMocker\Tests\Integration;
use PHPUnit\Framework\TestCase;
use Xepozz\InternalMocker\MockerState;
use function time;
final class TimeTest extends TestCase
{
public function testRun()
{
$this->assertEquals(`date +%s`, time());
}
public function testRun2()
{
MockerState::addCondition(
'',
'time',
[],
100
);
$this->assertEquals(100, time());
}
public function testRun3()
{
$this->assertEquals(`date +%s`, time());
}
public function testRun4()
{
$now = time();
sleep(1);
$next = time();
$this->assertEquals(1, $next - $now);
}
}
If someone wrote their own crutches or manually removed use function
from files in order to replace functions in the desired namespace, now you can get rid of them and replace it with connecting
Useful links
Documentation about disable-functions
: https://www.php.net/manual/en/ini.core.php#ini.disable-functions
Internal mocker: https://github.com/xepozz/internal-mocker/
And this post was written weeks ago in my Telegram channel: https://t.me/handle_topic 😉
This is translation of the original post in https://habr.com/ru/articles/797343/
Top comments (0)