DEV Community

Cover image for A way to mock PHP internal functions with xepozz/internal-mocker
Dmitriy Derepko
Dmitriy Derepko

Posted on

A way to mock PHP internal functions with xepozz/internal-mocker

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

Enter fullscreen mode Exit fullscreen mode

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%

Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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`);
     }
}

Enter fullscreen mode Exit fullscreen mode

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);
     }
}
Enter fullscreen mode Exit fullscreen mode

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)