Skip to content

Commit 47b197b

Browse files
authored
Merge pull request #1 from xepozz/support-internal-functions
Support internal functions mock
2 parents 41aea59 + cc5d0b5 commit 47b197b

File tree

6 files changed

+156
-94
lines changed

6 files changed

+156
-94
lines changed

README.md

Lines changed: 45 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Introduction
22

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

66
## Installation
77

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

1212
## Usage
1313

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

16-
### Register hook
16+
### Register a hook
1717

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

5959
The package supports a few ways to mock functions:
6060

61-
1. Runtime mock
62-
2. Pre-defined mock
61+
1. Runtime mocks
62+
2. Pre-defined mocks
6363
3. Mix of two previous ways
6464

65-
#### Runtime mock
65+
#### Runtime mocks
6666

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

69-
Back to the created `MockerExtension::executeBeforeFirstTest` and edit the `$mocks` var.
69+
Back to the created `MockerExtension::executeBeforeFirstTest` and edit the `$mocks` variable.
7070

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

8484
```php
8585
MockerState::addCondition(
86-
'App\Service',
87-
'time',
88-
[],
89-
100
86+
'App\Service', // namespace
87+
'time', // function name
88+
[], // arguments
89+
100 // result
9090
);
9191
```
9292

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

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

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

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

165-
## Restrictions
165+
## Global namespaced functions
166166

167-
You should use function without using root namespace aliasing.
167+
### Internal functions
168168

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

171-
```php
172-
namespace App\Service
174+
#### Workaround
173175

174-
class SomeService
175-
{
176-
public function doSomething()
177-
{
178-
// ...
179-
time()
180-
// ...
181-
}
182-
}
183-
```
176+
The way you can mock global functions is to disable them
177+
in `php.ini`: https://www.php.net/manual/en/ini.core.php#ini.disable-functions
184178

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

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

189-
```php
190-
namespace App\Service
185+
> Replace `${functions}` with the list of functions that you want to mock, separated by commas, e.g.: `time,rand`.
191186
192-
class SomeService
193-
{
194-
public function doSomething()
195-
{
196-
// ...
197-
\time()
198-
// ...
199-
}
200-
}
201-
```
187+
So now you can mock global functions as well.
202188

203-
Make sure that function isn't included into `use` section.
189+
#### Internal function implementation
204190

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

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

210-
class SomeService
211-
{
212-
public function doSomething()
213-
{
214-
// ...
215-
time()
216-
// ...
217-
}
218-
}
195+
The shortest way to implement a function is to use ``` `bash command` ``` syntax:
196+
197+
```php
198+
$mocks[] = [
199+
'namespace' => '',
200+
'name' => 'time',
201+
'function' => fn () => `date +%s`,
202+
];
219203
```
220204

221-
##### Data Providers
205+
## Restrictions
206+
207+
### Data Providers
222208

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

267253
That all because of PHPUnit 9.5 and lower event management system.
268-
Data Provider functionality starts to work before any events so it's impossible to mock the function at the beginning of
254+
Data Provider functionality starts to work before any events, so it's impossible to mock the function at the beginning
255+
of
269256
the runtime.

composer.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
{
22
"name": "xepozz/internal-mocker",
33
"type": "library",
4-
"autoload": {
5-
"psr-4": {
6-
"Xepozz\\InternalMocker\\": "src/"
7-
}
8-
},
9-
"autoload-dev": {
10-
"psr-4": {
11-
"Xepozz\\InternalMocker\\Tests\\": "tests/"
12-
}
13-
},
144
"authors": [
155
{
166
"name": "Dmitrii Derepko",
177
"email": "xepozz@list.ru"
188
}
199
],
10+
"require": {
11+
"yiisoft/var-dumper": "^1.2"
12+
},
2013
"require-dev": {
2114
"phpunit/phpunit": "^9.5"
2215
},
23-
"require": {
24-
"yiisoft/var-dumper": "^1.2"
16+
"autoload": {
17+
"psr-4": {
18+
"Xepozz\\InternalMocker\\": "src/"
19+
}
20+
},
21+
"autoload-dev": {
22+
"psr-4": {
23+
"Xepozz\\InternalMocker\\Tests\\": "tests/"
24+
}
2525
}
2626
}

src/Mocker.php

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public function load(array $mocks): void
3030
public function generate(array $mocks): string
3131
{
3232
$mocks = $this->normalizeMocks($mocks);
33-
$mockerConfig = ['namespace ' . __NAMESPACE__ . ';'];
33+
$mockerConfig = [];
3434
foreach ($mocks as $namespace => $functions) {
3535
foreach ($functions as $functionName => $imocks) {
3636
foreach ($imocks as $imock) {
@@ -41,39 +41,52 @@ public function generate(array $mocks): string
4141
$resultString = VarDumper::create($imock['result'])->export(false);
4242
$defaultString = $imock['default'] ? 'true' : 'false';
4343
$mockerConfig[] = <<<PHP
44-
MockerState::addCondition(
45-
"$namespace",
46-
"$functionName",
47-
$argumentsString,
48-
$resultString,
49-
$defaultString,
50-
);
51-
PHP;
44+
MockerState::addCondition(
45+
"$namespace",
46+
"$functionName",
47+
$argumentsString,
48+
$resultString,
49+
$defaultString,
50+
);
51+
PHP;
5252
}
5353
}
5454
}
5555
$outputs = [];
56+
$mockerConfigClassName = MockerState::class;
5657
foreach ($mocks as $namespace => $functions) {
5758
$innerOutputsString = $this->generateFunction($functions);
5859

59-
$mockerConfigClassName = MockerState::class;
60-
6160
$outputs[] = <<<PHP
62-
namespace {$namespace};
61+
namespace {$namespace} {
62+
use {$mockerConfigClassName};
63+
64+
$innerOutputsString
65+
}
66+
PHP;
67+
}
6368

64-
use $mockerConfigClassName;
6569

66-
$innerOutputsString
67-
PHP;
70+
$pre = '';
71+
if ($mockerConfig !== []) {
72+
$runtimeMocks = implode("\n", $mockerConfig);
73+
$pre = <<<PHP
74+
namespace {
75+
use {$mockerConfigClassName};
76+
77+
{$runtimeMocks}
78+
}
79+
PHP;
6880
}
6981

7082

71-
return implode("\n", $mockerConfig) . "\n\n\n" . implode("\n", $outputs);
83+
return $pre . "\n\n\n" . implode("\n", $outputs);
7284
}
7385

7486
private function normalizeMocks(array $mocks): array
7587
{
7688
$result = [];
89+
usort($mocks, fn ($a, $b) => strlen($a['namespace']) <=> strlen($b['namespace']));
7790
foreach ($mocks as $mock) {
7891
$result[$mock['namespace']][$mock['name']][] = [
7992
'namespace' => $mock['namespace'],
@@ -82,6 +95,7 @@ private function normalizeMocks(array $mocks): array
8295
'arguments' => $mock['arguments'] ?? [],
8396
'skip' => !array_key_exists('result', $mock),
8497
'default' => $mock['default'] ?? false,
98+
'function' => $mock['function'] ?? false,
8599
];
86100
}
87101
return $result;
@@ -91,13 +105,20 @@ private function generateFunction(mixed $groupedMocks): string
91105
{
92106
$innerOutputs = [];
93107
foreach ($groupedMocks as $functionName => $_) {
108+
$function = "fn() => \\$functionName(...\$arguments)";
109+
if ($_[0]['function'] !== false) {
110+
$function = is_string($_[0]['function']) ? $_[0]['function'] : VarDumper::create(
111+
$_[0]['function']
112+
)->export(false);
113+
}
114+
94115
$string = <<<PHP
95116
function $functionName(...\$arguments)
96117
{
97118
if (MockerState::checkCondition(__NAMESPACE__, "$functionName", \$arguments)) {
98119
return MockerState::getResult(__NAMESPACE__, "$functionName", \$arguments);
99120
}
100-
return MockerState::getDefaultResult(__NAMESPACE__, "$functionName", fn() => \\$functionName(...\$arguments));
121+
return MockerState::getDefaultResult(__NAMESPACE__, "$functionName", $function);
101122
}
102123
PHP;
103124
$innerOutputs[] = $string;

tests/Integration/TimeTest.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Xepozz\InternalMocker\Tests\Integration;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use Xepozz\InternalMocker\MockerState;
9+
10+
use function time;
11+
12+
final class TimeTest extends TestCase
13+
{
14+
public function testRun()
15+
{
16+
$this->assertEquals(`date +%s`, time());
17+
}
18+
19+
public function testRun2()
20+
{
21+
MockerState::addCondition(
22+
'',
23+
'time',
24+
[],
25+
100
26+
);
27+
28+
$this->assertEquals(100, time());
29+
}
30+
31+
public function testRun3()
32+
{
33+
$this->assertEquals(`date +%s`, time());
34+
}
35+
36+
public function testRun4()
37+
{
38+
$now = time();
39+
sleep(1);
40+
$next = time();
41+
42+
$this->assertEquals(1, $next - $now);
43+
}
44+
}

tests/MockerExtension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ public static function load(): void
6969
'namespace' => 'ASD',
7070
'name' => 'only_runtime',
7171
],
72+
[
73+
'namespace' => '',
74+
'name' => 'time',
75+
'function' => fn () => `date +%s`,
76+
],
7277
];
7378

7479
$mocker = new Mocker();

0 commit comments

Comments
 (0)