Skip to content

Commit

Permalink
Merge pull request #1 from xepozz/support-internal-functions
Browse files Browse the repository at this point in the history
Support internal functions mock
  • Loading branch information
xepozz authored Dec 22, 2023
2 parents 41aea59 + cc5d0b5 commit 47b197b
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 94 deletions.
103 changes: 45 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Introduction

The package helps mock internal php functions as simple as it can. You can use this package when you need mock such
functions as: `time()`, `str_contains()` and etc.
The package helps mock internal php functions as simple as possible. Use this package when you need mock such
functions as: `time()`, `str_contains()`, `rand`, etc.

## Installation

Expand All @@ -11,9 +11,9 @@ composer require xepozz/internal-mocker --dev

## Usage

The main idea is simple: register Listener of PHPUnit and call Mocker at first.
The main idea is pretty simple: register a Listener for PHPUnit and call the Mocker extension first.

### Register hook
### Register a hook

1. Create new file `tests/MockerExtension.php`
2. Paste the following code into the created file:
Expand Down Expand Up @@ -58,15 +58,15 @@ Here you have registered extension that will be called every time when you run `

The package supports a few ways to mock functions:

1. Runtime mock
2. Pre-defined mock
1. Runtime mocks
2. Pre-defined mocks
3. Mix of two previous ways

#### Runtime mock
#### Runtime mocks

If you want to make your test case to be used with mocked function you should register it before.

Back to the created `MockerExtension::executeBeforeFirstTest` and edit the `$mocks` var.
Back to the created `MockerExtension::executeBeforeFirstTest` and edit the `$mocks` variable.

```php
$mocks = [
Expand All @@ -83,10 +83,10 @@ When you want to mock result in tests you should write the following code into n

```php
MockerState::addCondition(
'App\Service',
'time',
[],
100
'App\Service', // namespace
'time', // function name
[], // arguments
100 // result
);
```

Expand Down Expand Up @@ -127,9 +127,9 @@ Pre-defined mocks allow you to mock behaviour globally.
It means that you don't need to write `MockerState::addCondition(...)` into each test case if you want to mock it for
whole project.

> Keep in the mind that the same function in different namespaces is not the same for `Mocker`.
> Keep in mind that the same functions from different namespaces are not the same for `Mocker`.
So back to the created `MockerExtension::executeBeforeFirstTest` and edit the `$mocks` var.
So back to the created `MockerExtension::executeBeforeFirstTest` and edit the `$mocks` variable.

```php
$mocks = [
Expand Down Expand Up @@ -162,63 +162,49 @@ These methods save "current" state and unload each `Runtime mock` mock that was

Using `MockerState::saveState()` after `Mocker->load($mocks)` saves only **_Pre-defined_** mocks.

## Restrictions
## Global namespaced functions

You should use function without using root namespace aliasing.
### Internal functions

#### Good example
Without any additional configuration you can mock only functions that are defined under any not global
namespaces: `App\`, `App\Service\`, etc.
But you cannot mock functions that are defined under global namespace or defined in a `use` statement, e.g. `use time;`
or `\time();`.

```php
namespace App\Service
#### Workaround

class SomeService
{
public function doSomething()
{
// ...
time()
// ...
}
}
```
The way you can mock global functions is to disable them
in `php.ini`: https://www.php.net/manual/en/ini.core.php#ini.disable-functions

#### Bad examples
The best way is to disable them only for tests by running a command with the additional flags:

Make sure that function doesn't have leading backslash.
```bash
php -ddisable_functions=${functions} ./vendor/bin/phpunit
```

```php
namespace App\Service
> Replace `${functions}` with the list of functions that you want to mock, separated by commas, e.g.: `time,rand`.
class SomeService
{
public function doSomething()
{
// ...
\time()
// ...
}
}
```
So now you can mock global functions as well.

Make sure that function isn't included into `use` section.
#### Internal function implementation

```php
namespace App\Service
When you disable a function in `php.ini` you cannot call it anymore. That means you must implement it by yourself.

use function time;
Obviously, almost all functions are implemented in PHP looks the same as the Bash ones.

class SomeService
{
public function doSomething()
{
// ...
time()
// ...
}
}
The shortest way to implement a function is to use ``` `bash command` ``` syntax:

```php
$mocks[] = [
'namespace' => '',
'name' => 'time',
'function' => fn () => `date +%s`,
];
```

##### Data Providers
## Restrictions

### Data Providers

Sometimes you may face unpleasant situation when mocked function is not mocking without forced using `namespace`
+ `function`.
Expand Down Expand Up @@ -265,5 +251,6 @@ final class MockerExtension implements BeforeTestHook, BeforeFirstTestHook
```

That all because of PHPUnit 9.5 and lower event management system.
Data Provider functionality starts to work before any events so it's impossible to mock the function at the beginning of
Data Provider functionality starts to work before any events, so it's impossible to mock the function at the beginning
of
the runtime.
24 changes: 12 additions & 12 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
{
"name": "xepozz/internal-mocker",
"type": "library",
"autoload": {
"psr-4": {
"Xepozz\\InternalMocker\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Xepozz\\InternalMocker\\Tests\\": "tests/"
}
},
"authors": [
{
"name": "Dmitrii Derepko",
"email": "[email protected]"
}
],
"require": {
"yiisoft/var-dumper": "^1.2"
},
"require-dev": {
"phpunit/phpunit": "^9.5"
},
"require": {
"yiisoft/var-dumper": "^1.2"
"autoload": {
"psr-4": {
"Xepozz\\InternalMocker\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Xepozz\\InternalMocker\\Tests\\": "tests/"
}
}
}
55 changes: 38 additions & 17 deletions src/Mocker.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public function load(array $mocks): void
public function generate(array $mocks): string
{
$mocks = $this->normalizeMocks($mocks);
$mockerConfig = ['namespace ' . __NAMESPACE__ . ';'];
$mockerConfig = [];
foreach ($mocks as $namespace => $functions) {
foreach ($functions as $functionName => $imocks) {
foreach ($imocks as $imock) {
Expand All @@ -41,39 +41,52 @@ public function generate(array $mocks): string
$resultString = VarDumper::create($imock['result'])->export(false);
$defaultString = $imock['default'] ? 'true' : 'false';
$mockerConfig[] = <<<PHP
MockerState::addCondition(
"$namespace",
"$functionName",
$argumentsString,
$resultString,
$defaultString,
);
PHP;
MockerState::addCondition(
"$namespace",
"$functionName",
$argumentsString,
$resultString,
$defaultString,
);
PHP;
}
}
}
$outputs = [];
$mockerConfigClassName = MockerState::class;
foreach ($mocks as $namespace => $functions) {
$innerOutputsString = $this->generateFunction($functions);

$mockerConfigClassName = MockerState::class;

$outputs[] = <<<PHP
namespace {$namespace};
namespace {$namespace} {
use {$mockerConfigClassName};
$innerOutputsString
}
PHP;
}

use $mockerConfigClassName;

$innerOutputsString
PHP;
$pre = '';
if ($mockerConfig !== []) {
$runtimeMocks = implode("\n", $mockerConfig);
$pre = <<<PHP
namespace {
use {$mockerConfigClassName};
{$runtimeMocks}
}
PHP;
}


return implode("\n", $mockerConfig) . "\n\n\n" . implode("\n", $outputs);
return $pre . "\n\n\n" . implode("\n", $outputs);
}

private function normalizeMocks(array $mocks): array
{
$result = [];
usort($mocks, fn ($a, $b) => strlen($a['namespace']) <=> strlen($b['namespace']));
foreach ($mocks as $mock) {
$result[$mock['namespace']][$mock['name']][] = [
'namespace' => $mock['namespace'],
Expand All @@ -82,6 +95,7 @@ private function normalizeMocks(array $mocks): array
'arguments' => $mock['arguments'] ?? [],
'skip' => !array_key_exists('result', $mock),
'default' => $mock['default'] ?? false,
'function' => $mock['function'] ?? false,
];
}
return $result;
Expand All @@ -91,13 +105,20 @@ private function generateFunction(mixed $groupedMocks): string
{
$innerOutputs = [];
foreach ($groupedMocks as $functionName => $_) {
$function = "fn() => \\$functionName(...\$arguments)";
if ($_[0]['function'] !== false) {
$function = is_string($_[0]['function']) ? $_[0]['function'] : VarDumper::create(
$_[0]['function']
)->export(false);
}

$string = <<<PHP
function $functionName(...\$arguments)
{
if (MockerState::checkCondition(__NAMESPACE__, "$functionName", \$arguments)) {
return MockerState::getResult(__NAMESPACE__, "$functionName", \$arguments);
}
return MockerState::getDefaultResult(__NAMESPACE__, "$functionName", fn() => \\$functionName(...\$arguments));
return MockerState::getDefaultResult(__NAMESPACE__, "$functionName", $function);
}
PHP;
$innerOutputs[] = $string;
Expand Down
44 changes: 44 additions & 0 deletions tests/Integration/TimeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

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);
}
}
5 changes: 5 additions & 0 deletions tests/MockerExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ public static function load(): void
'namespace' => 'ASD',
'name' => 'only_runtime',
],
[
'namespace' => '',
'name' => 'time',
'function' => fn () => `date +%s`,
],
];

$mocker = new Mocker();
Expand Down
Loading

0 comments on commit 47b197b

Please sign in to comment.