├── generator
└── stub
│ ├── .gitignore
│ ├── Makefile
│ ├── patches.php
│ └── generator.php
├── Makefile
├── .gitignore
├── tests
├── Integration
│ ├── DateTime.php
│ ├── StrContains.php
│ ├── UseInSucceedDataProviderStub.php
│ ├── Stub.php
│ ├── HeaderTest.php
│ ├── HeadersSentTest.php
│ ├── DateTimeTest.php
│ ├── FunctionExistsTest.php
│ ├── TimeTest.php
│ ├── StrContainsTest.php
│ ├── EarlyMockInitializationSucceedTest.php
│ ├── IgnoreArgumentsTest.php
│ └── TraceTest.php
├── MockerExtension.php
└── MockerTest.php
├── composer.json
├── phpunit.xml
├── phpunit9.xml
├── .meta-storm.xml
├── .github
└── workflows
│ └── phpunit.yaml
├── src
├── MockerState.php
└── Mocker.php
└── README.md
/generator/stub/.gitignore:
--------------------------------------------------------------------------------
1 | stubs/
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | generate-stubs:
2 | cd generator/stub; make generate
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | /vendor
3 | /data/
4 | /tests/data/
5 |
6 | .phpunit.result.cache
7 | composer.lock
--------------------------------------------------------------------------------
/generator/stub/Makefile:
--------------------------------------------------------------------------------
1 | clone:
2 | git clone https://github.com/JetBrains/phpstorm-stubs stubs
3 | clean:
4 | rm -rf stubs
5 | generator:
6 | php generator.php
7 |
8 | generate: clean clone generator
--------------------------------------------------------------------------------
/tests/Integration/DateTime.php:
--------------------------------------------------------------------------------
1 | [
8 | * 'signatureArguments' => '?string $name = null',
9 | * 'arguments' => '$name',
10 | * ],
11 | */
12 | return [
13 | ];
--------------------------------------------------------------------------------
/tests/Integration/StrContains.php:
--------------------------------------------------------------------------------
1 | assertEquals(['Content-Type: application/json', true, 0], $trace[0]['arguments']);
22 | }
23 | }
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "xepozz/internal-mocker",
3 | "type": "library",
4 | "authors": [
5 | {
6 | "name": "Dmitrii Derepko",
7 | "email": "xepozz@list.ru"
8 | }
9 | ],
10 | "require": {
11 | "yiisoft/var-dumper": "^1.2"
12 | },
13 | "require-dev": {
14 | "phpunit/phpunit": "^9 || ^10 || ^11"
15 | },
16 | "autoload": {
17 | "psr-4": {
18 | "Xepozz\\InternalMocker\\": "src/"
19 | }
20 | },
21 | "autoload-dev": {
22 | "psr-4": {
23 | "Xepozz\\InternalMocker\\Tests\\": "tests/"
24 | }
25 | },
26 | "scripts": {
27 | "test": "php -ddisable_functions=time,header,headers_sent vendor/bin/phpunit --random-order-seed=1"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/Integration/HeadersSentTest.php:
--------------------------------------------------------------------------------
1 | $file = $line = 123,
19 | );
20 | headers_sent($file, $line);
21 |
22 | $trace = MockerState::getTraces(
23 | '',
24 | 'headers_sent',
25 | );
26 |
27 | $this->assertEquals(123, $file);
28 | $this->assertEquals(123, $line);
29 | $this->assertEquals([123, 123], $trace[0]['arguments']);
30 | }
31 | }
--------------------------------------------------------------------------------
/tests/Integration/DateTimeTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(555, $obj->run());
16 | }
17 |
18 | public function testRun2()
19 | {
20 | $obj = new DateTime();
21 |
22 | MockerState::addCondition(
23 | __NAMESPACE__,
24 | 'time',
25 | [],
26 | 100
27 | );
28 |
29 | $this->assertEquals(100, $obj->run());
30 | }
31 |
32 | public function testRun3()
33 | {
34 | $obj = new DateTime();
35 |
36 | $this->assertEquals(555, $obj->run());
37 | }
38 | }
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | ./tests/
15 |
16 |
17 |
18 |
19 | ./src
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/tests/Integration/FunctionExistsTest.php:
--------------------------------------------------------------------------------
1 | expectException(RuntimeException::class);
23 | new Stub();
24 | }
25 |
26 | public function testFail()
27 | {
28 | MockerState::addCondition(
29 | __NAMESPACE__,
30 | 'function_exists',
31 | ['function_exists'],
32 | true
33 | );
34 |
35 | new Stub();
36 | $this->assertTrue(true);
37 | }
38 | }
--------------------------------------------------------------------------------
/phpunit9.xml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | ./tests/
23 |
24 |
25 |
26 |
27 |
28 | ./src
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/tests/Integration/TimeTest.php:
--------------------------------------------------------------------------------
1 | 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/Integration/StrContainsTest.php:
--------------------------------------------------------------------------------
1 | assertFalse($obj->run('string', 'str'));
17 | }
18 |
19 | public function testRun2(): void
20 | {
21 | $obj = new StrContains();
22 |
23 | $this->assertFalse($obj->run('string2', 'str'));
24 | }
25 |
26 | public function testRun3(): void
27 | {
28 | $obj = new StrContains();
29 |
30 | $this->assertTrue($obj->run('string3', 'str'));
31 | }
32 |
33 | public function testRun4(): void
34 | {
35 | $obj = new StrContains();
36 |
37 | MockerState::addCondition(
38 | __NAMESPACE__,
39 | 'str_contains',
40 | ['string4', 'str'],
41 | false
42 | );
43 |
44 | $this->assertFalse($obj->run('string4', 'str'));
45 | }
46 | }
--------------------------------------------------------------------------------
/tests/Integration/EarlyMockInitializationSucceedTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('not serialized', $object->run('asd'));
33 | }
34 |
35 | public static function dataProvider(): iterable
36 | {
37 | return [
38 | [new UseInSucceedDataProviderStub()],
39 | ];
40 | }
41 | }
--------------------------------------------------------------------------------
/tests/Integration/IgnoreArgumentsTest.php:
--------------------------------------------------------------------------------
1 | assertTrue(class_exists($class));
27 | }
28 |
29 | /**
30 | * @dataProvider dataProvider
31 | */
32 | #[DataProvider('dataProvider')]
33 | public function testSuccessByDefault(string $class): void
34 | {
35 | MockerState::addCondition(
36 | __NAMESPACE__,
37 | 'class_exists',
38 | [],
39 | true,
40 | true
41 | );
42 |
43 | $this->assertTrue(class_exists($class));
44 | }
45 |
46 | public static function dataProvider(): iterable
47 | {
48 | return [
49 | ['A'],
50 | ['B'],
51 | ['[{}]'],
52 | ];
53 | }
54 | }
--------------------------------------------------------------------------------
/.meta-storm.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/.github/workflows/phpunit.yaml:
--------------------------------------------------------------------------------
1 | on:
2 | pull_request:
3 | push:
4 |
5 | name: "PHPUnit"
6 |
7 | jobs:
8 | phpunit:
9 | name: PHPUnit ${{ matrix.phpunit }} PHP ${{ matrix.php }}-${{ matrix.os }}
10 | runs-on: ${{ matrix.os }}
11 | strategy:
12 | matrix:
13 | os:
14 | - ubuntu-latest
15 | php:
16 | - "8.0"
17 | - "8.1"
18 | - "8.2"
19 | - "8.3"
20 | phpunit:
21 | - 9
22 | - 10
23 | - 11
24 | exclude:
25 | - php: "8.0"
26 | phpunit: 10
27 | - php: "8.0"
28 | phpunit: 11
29 | - php: "8.1"
30 | phpunit: 11
31 | steps:
32 | - name: Checkout
33 | uses: actions/checkout@v2.3.4
34 |
35 | - name: Install PHP
36 | uses: shivammathur/setup-php@v2
37 | with:
38 | php-version: ${{ matrix.php }}
39 | tools: composer:v2
40 | coverage: pcov
41 |
42 | - name: Install dependencies
43 | run: composer install
44 |
45 | - name: Switching PHPUnit version
46 | run: composer req --dev phpunit/phpunit:^${{ matrix.phpunit }} -W
47 |
48 | - name: Run tests with code coverage.
49 | if: matrix.phpunit == '9'
50 | run: composer test -- -c phpunit9.xml
51 |
52 | - name: Run tests with code coverage.
53 | if: matrix.phpunit != '9'
54 | run: composer test -- -c phpunit.xml
55 |
56 | - name: Upload coverage to Codecov.
57 | if: matrix.os == 'ubuntu-latest'
58 | uses: codecov/codecov-action@v3
59 | with:
60 | files: ./coverage.xml
61 |
--------------------------------------------------------------------------------
/generator/stub/generator.php:
--------------------------------------------------------------------------------
1 | $signatureArguments[1] ?? '',
43 | 'arguments' => implode(', ', $arguments[0] ?? []),
44 | ];
45 | }
46 | }
47 |
48 | $patches = require './patches.php';
49 |
50 | $result = array_merge($stubs, $patches);
51 |
52 | file_put_contents(
53 | $destination,
54 | 'run('test');
23 | $object->run('test2');
24 | $object->run('test3');
25 |
26 | $traces = MockerState::getTraces(
27 | __NAMESPACE__,
28 | 'serialize',
29 | );
30 |
31 | $this->assertIsArray($traces);
32 |
33 | $this->assertCount(3, $traces);
34 |
35 | foreach ($traces as $trace) {
36 | $this->assertArrayHasKey('arguments', $trace);
37 | $this->assertArrayHasKey('result', $trace);
38 |
39 | $this->assertIsArray($trace['arguments']);
40 | $this->assertIsArray($trace['trace']);
41 | $this->assertIsString($trace['result']);
42 | }
43 |
44 | $this->assertEquals(['test'], $traces[0]['arguments']);
45 | $this->assertEquals('s:4:"test";', $traces[0]['result']);
46 |
47 | $this->assertEquals(['test2'], $traces[1]['arguments']);
48 | $this->assertEquals('s:5:"test2";', $traces[1]['result']);
49 |
50 | $this->assertEquals(['test3'], $traces[2]['arguments']);
51 | $this->assertEquals('s:5:"test3";', $traces[2]['result']);
52 | }
53 |
54 | public function testBacktrace(): void
55 | {
56 | $object = new UseInSucceedDataProviderStub();
57 | $object->run('test');
58 |
59 | $traces = MockerState::getTraces(
60 | __NAMESPACE__,
61 | 'serialize',
62 | );
63 |
64 | $this->assertIsArray($traces);
65 |
66 | $this->assertCount(1, $traces);
67 |
68 | $this->assertEquals(['test'], $traces[0]['arguments']);
69 | $this->assertEquals('saveTrace', $traces[0]['trace'][0]['function']);
70 | $this->assertEquals(144, $traces[0]['trace'][0]['line']);
71 | $this->assertEquals(
72 | [
73 | __NAMESPACE__,
74 | 'serialize',
75 | 'test',
76 | ],
77 | $traces[0]['trace'][0]['args'],
78 | );
79 |
80 | $this->assertEquals(__NAMESPACE__ . '\serialize', $traces[0]['trace'][1]['function']);
81 | $this->assertEquals(11, $traces[0]['trace'][1]['line']);
82 | $this->assertEquals(['test'], $traces[0]['trace'][1]['args']);
83 | }
84 | }
--------------------------------------------------------------------------------
/tests/MockerExtension.php:
--------------------------------------------------------------------------------
1 | registerSubscribers(
48 | new class () implements StartedSubscriber {
49 | public function notify(Started $event): void
50 | {
51 | MockerExtension::load();
52 | }
53 | },
54 | new class implements PreparationStartedSubscriber {
55 | public function notify(PreparationStarted $event): void
56 | {
57 | MockerState::resetState();
58 | }
59 | },
60 | );
61 | }
62 |
63 | public static function load(): void
64 | {
65 | $mocks = [
66 | [
67 | 'namespace' => 'Xepozz\InternalMocker\Tests\Integration',
68 | 'name' => 'function_exists',
69 | ],
70 | [
71 | 'namespace' => 'Xepozz\InternalMocker\Tests\Integration',
72 | 'name' => 'class_exists',
73 | 'default' => true,
74 | 'result' => true,
75 | ],
76 | [
77 | 'namespace' => 'Xepozz\\InternalMocker\\Tests\\Integration',
78 | 'name' => 'time',
79 | 'arguments' => [],
80 | 'result' => 555,
81 | ],
82 | [
83 | 'namespace' => 'Xepozz\\InternalMocker\\Tests\\Integration',
84 | 'name' => 'str_contains',
85 | 'arguments' => [
86 | 'haystack' => 'string',
87 | 'needle' => 'str',
88 | ],
89 | 'result' => false,
90 | ],
91 | [
92 | 'namespace' => 'Xepozz\\InternalMocker\\Tests\\Integration',
93 | 'name' => 'serialize',
94 | ],
95 | [
96 | 'namespace' => 'Xepozz\\InternalMocker\\Tests\\Integration',
97 | 'name' => 'unserialize',
98 | ],
99 | [
100 | 'namespace' => 'Xepozz\\InternalMocker\\Tests\\Integration',
101 | 'name' => 'str_contains',
102 | 'arguments' => [
103 | 'haystack' => 'string2',
104 | 'needle' => 'str',
105 | ],
106 | 'result' => false,
107 | ],
108 | [
109 | 'namespace' => 'ASD',
110 | 'name' => 'only_runtime',
111 | ],
112 | [
113 | 'namespace' => '',
114 | 'name' => 'time',
115 | 'function' => fn () => `date +%s`,
116 | ],
117 | [
118 | 'namespace' => '',
119 | 'name' => 'header',
120 | 'function' => fn (string $value) => true,
121 | ],
122 | [
123 | 'namespace' => '',
124 | 'name' => 'headers_sent',
125 | ],
126 | ];
127 |
128 | $mocker = new Mocker();
129 | $mocker->load($mocks);
130 | MockerState::saveState();
131 | }
132 |
133 | }
--------------------------------------------------------------------------------
/src/MockerState.php:
--------------------------------------------------------------------------------
1 | $namespace,
33 | 'name' => $functionName,
34 | 'result' => $result,
35 | 'arguments' => $arguments,
36 | ];
37 | }
38 |
39 | public static function checkCondition(
40 | string $namespace,
41 | string $functionName,
42 | array|string $expectedArguments,
43 | ): bool {
44 | $mocks = self::$state[$namespace][$functionName] ?? [];
45 |
46 | foreach ($mocks as $mock) {
47 | if (self::compareArguments($mock, $expectedArguments)) {
48 | return true;
49 | }
50 | }
51 | return false;
52 | }
53 |
54 | private static function compareArguments(array $arguments, array|string $expectedArguments): bool
55 | {
56 | return $arguments['arguments'] === $expectedArguments
57 | || (is_array($arguments['arguments']) && array_values($arguments['arguments']) === $expectedArguments);
58 | }
59 |
60 | private static function replaceResult(
61 | string $namespace,
62 | string $functionName,
63 | array $arguments,
64 | mixed $result,
65 | ): void {
66 | $mocks = &self::$state[$namespace][$functionName];
67 |
68 | foreach ($mocks as &$mock) {
69 | if ($mock['arguments'] === $arguments) {
70 | $mock['result'] = $result;
71 | }
72 | }
73 | }
74 |
75 | public static function getResult(
76 | string $namespace,
77 | string $functionName,
78 | &...$expectedArguments,
79 | ): mixed {
80 | $mocks = self::$state[$namespace][$functionName] ?? [];
81 |
82 | foreach ($mocks as $mock) {
83 | if (self::compareArguments($mock, $expectedArguments)) {
84 | return is_callable($mock['result']) ? $mock['result'](...$expectedArguments) : $mock['result'];
85 | }
86 | }
87 | return false;
88 | }
89 |
90 | public static function getDefaultResult(
91 | string $namespace,
92 | string $functionName,
93 | callable $fallback,
94 | &...$arguments,
95 | ): mixed {
96 | if (isset(self::$defaults[$namespace][$functionName])) {
97 | return self::$defaults[$namespace][$functionName];
98 | }
99 |
100 | return $fallback(...$arguments);
101 | }
102 |
103 | public static function saveState(): void
104 | {
105 | self::$savedState = self::$state;
106 | }
107 |
108 | public static function resetState(): void
109 | {
110 | self::$state = self::$savedState;
111 | self::$traces = [];
112 | }
113 |
114 | public static function saveTrace(
115 | string $namespace,
116 | string $functionName,
117 | &...$arguments
118 | ): int {
119 | $position = count(self::$traces[$namespace][$functionName] ?? []);
120 | self::$traces[$namespace][$functionName][$position] = [
121 | 'arguments' => &$arguments,
122 | 'trace' => debug_backtrace(),
123 | ];
124 |
125 | return $position;
126 | }
127 |
128 | public static function saveTraceResult(
129 | string $namespace,
130 | string $functionName,
131 | int $position,
132 | mixed $result
133 | ): mixed {
134 | self::$traces[$namespace][$functionName][$position]['result'] = $result;
135 |
136 | return $result;
137 | }
138 |
139 | public static function getTraces(
140 | string $namespace,
141 | string $functionName
142 | ): array {
143 | return self::$traces[$namespace][$functionName] ?? [];
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/tests/MockerTest.php:
--------------------------------------------------------------------------------
1 | assertEquals($expected, $mocker->generate($mocks));
21 | }
22 |
23 | public static function generateProvider(): iterable
24 | {
25 | return [
26 | [
27 | [
28 | [
29 | 'namespace' => 'Xepozz\InternalMocker\Tests\Integration',
30 | 'name' => 'time',
31 | 'result' => 555,
32 | 'arguments' => [],
33 | ],
34 | ],
35 | << \\time(), );
59 | }
60 |
61 | return MockerState::saveTraceResult(__NAMESPACE__, "time", \$position, \$result);
62 | }
63 | }
64 | PHP,
65 | ],
66 | [
67 | [
68 | [
69 | 'namespace' => 'Xepozz\InternalMocker\Tests\Integration',
70 | 'name' => 'str_contains',
71 | 'result' => false,
72 | 'arguments' => [
73 | 'haystack' => 'string',
74 | 'needle' => 'str',
75 | ],
76 | ],
77 | [
78 | 'namespace' => 'Xepozz\InternalMocker\Tests\Integration',
79 | 'name' => 'str_contains',
80 | 'result' => false,
81 | 'arguments' => [
82 | 'haystack' => 'string2',
83 | 'needle' => 'str',
84 | ],
85 | ],
86 | ],
87 | << 'string','needle' => 'str'],
95 | false,
96 | false,
97 | );
98 | MockerState::addCondition(
99 | "Xepozz\InternalMocker\Tests\Integration",
100 | "str_contains",
101 | ['haystack' => 'string2','needle' => 'str'],
102 | false,
103 | false,
104 | );
105 | }
106 |
107 |
108 | namespace Xepozz\InternalMocker\Tests\Integration {
109 | use Xepozz\InternalMocker\MockerState;
110 |
111 | function str_contains(string \$haystack, string \$needle)
112 | {
113 | \$position = MockerState::saveTrace(__NAMESPACE__, "str_contains", \$haystack, \$needle);
114 | if (MockerState::checkCondition(__NAMESPACE__, "str_contains", [\$haystack, \$needle])) {
115 | \$result = MockerState::getResult(__NAMESPACE__, "str_contains", \$haystack, \$needle);
116 | } else {
117 | \$result = MockerState::getDefaultResult(__NAMESPACE__, "str_contains", fn(string \$haystack, string \$needle) => \\str_contains(\$haystack, \$needle), \$haystack, \$needle);
118 | }
119 |
120 | return MockerState::saveTraceResult(__NAMESPACE__, "str_contains", \$position, \$result);
121 | }
122 | }
123 | PHP,
124 | ],
125 | ];
126 | }
127 | }
--------------------------------------------------------------------------------
/src/Mocker.php:
--------------------------------------------------------------------------------
1 | generate($mocks);
21 | $configPath = $this->getConfigPath();
22 | $directoryPath = dirname($configPath);
23 |
24 | if (!is_dir($directoryPath)) {
25 | mkdir($directoryPath, 0775, true);
26 | }
27 | file_put_contents($configPath, $data);
28 |
29 | require_once $configPath;
30 | }
31 |
32 | public function generate(array $mocks): string
33 | {
34 | $mocks = $this->normalizeMocks($mocks);
35 | $mockerConfig = [];
36 | foreach ($mocks as $namespace => $functions) {
37 | foreach ($functions as $functionName => $imocks) {
38 | foreach ($imocks as $imock) {
39 | if ($imock['skip']) {
40 | continue;
41 | }
42 | $argumentsString = VarDumper::create($imock['arguments'])->export(false);
43 | $resultString = VarDumper::create($imock['result'])->export(false);
44 | $defaultString = $imock['default'] ? 'true' : 'false';
45 | $mockerConfig[] = <<stubPath;
59 |
60 | $outputs = [];
61 | $mockerConfigClassName = MockerState::class;
62 | foreach ($mocks as $namespace => $functions) {
63 | $innerOutputsString = $this->generateFunction($functions, $stubs);
64 |
65 | $outputs[] = << strlen($a['namespace']) <=> strlen($b['namespace']));
95 | foreach ($mocks as $mock) {
96 | $result[$mock['namespace']][$mock['name']][] = [
97 | 'namespace' => $mock['namespace'],
98 | 'name' => $mock['name'],
99 | 'result' => $mock['result'] ?? null,
100 | 'arguments' => $mock['arguments'] ?? [],
101 | 'skip' => !array_key_exists('result', $mock),
102 | 'default' => $mock['default'] ?? false,
103 | 'function' => $mock['function'] ?? false,
104 | ];
105 | }
106 | return $result;
107 | }
108 |
109 | private function generateFunction(array $groupedMocks, array $stubs): string
110 | {
111 | $innerOutputs = [];
112 | foreach ($groupedMocks as $functionName => $_) {
113 | $signatureArguments = $stubs[$functionName]['signatureArguments'] ?? '...$arguments';
114 | if (isset($stubs[$functionName]['arguments'])) {
115 | $arrayArguments = sprintf('[%s]', $stubs[$functionName]['arguments']);
116 | $unpackedArguments = $stubs[$functionName]['arguments'];
117 | } else {
118 | $arrayArguments = '$arguments';
119 | $unpackedArguments = '...$arguments';
120 | }
121 |
122 | $function = "fn($signatureArguments) => \\$functionName($unpackedArguments)";
123 | if ($_[0]['function'] !== false) {
124 | $function = is_string($_[0]['function'])
125 | ? $_[0]['function']
126 | : VarDumper::create($_[0]['function'])->export(false);
127 | }
128 |
129 | $string = <<path;
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
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.
5 |
6 | [](https://packagist.org/packages/xepozz/internal-mocker)
7 | [](https://packagist.org/packages/xepozz/internal-mocker)
8 | [](https://github.com/xepozz/internal-mocker/actions)
9 |
10 | # Table of contents
11 |
12 | - [Installation](#installation)
13 | - [Usage](#usage)
14 | - [Register a PHPUnit Extension](#register-a-phpunit-extension)
15 | - [PHPUnit 9](#phpunit-9)
16 | - [PHPUnit 10 and higher](#phpunit-10-and-higher)
17 | - [Register mocks](#register-mocks)
18 | - [Runtime mocks](#runtime-mocks)
19 | - [Pre-defined mock](#pre-defined-mock)
20 | - [Mix of two previous ways](#mix-of-two-previous-ways)
21 | - [State](#state)
22 | - [Tracking calls](#tracking-calls)
23 | - [Global namespaced functions](#global-namespaced-functions)
24 | - [Internal functions](#internal-functions)
25 | - [Internal function implementation](#internal-function-implementation)
26 | - [Restrictions](#restrictions)
27 | - [Data Providers](#data-providers)
28 |
29 | ## Installation
30 |
31 | ```bash
32 | composer require xepozz/internal-mocker --dev
33 | ```
34 |
35 | ## Usage
36 |
37 | The main idea is pretty simple: register a Listener for PHPUnit and call the Mocker extension first.
38 |
39 | ### Register a PHPUnit Extension
40 |
41 | #### PHPUnit 9
42 |
43 | 1. Create new file `tests/MockerExtension.php`
44 | 2. Paste the following code into the created file:
45 | ```php
46 | load($mocks);
64 | MockerState::saveState();
65 | }
66 |
67 | public function executeBeforeTest(string $test): void
68 | {
69 | MockerState::resetState();
70 | }
71 | }
72 | ```
73 | 3. Register the hook as extension in `phpunit.xml.dist`
74 | ```xml
75 |
76 |
77 |
78 | ```
79 |
80 | #### PHPUnit 10 and higher
81 |
82 | 1. Create new file `tests/MockerExtension.php`
83 | 2. Paste the following code into the created file:
84 | ```php
85 | registerSubscribers(
106 | new class () implements StartedSubscriber {
107 | public function notify(Started $event): void
108 | {
109 | MockerExtension::load();
110 | }
111 | },
112 | new class implements PreparationStartedSubscriber {
113 | public function notify(PreparationStarted $event): void
114 | {
115 | MockerState::resetState();
116 | }
117 | },
118 | );
119 | }
120 |
121 | public static function load(): void
122 | {
123 | $mocks = [];
124 |
125 | $mocker = new Mocker();
126 | $mocker->load($mocks);
127 | MockerState::saveState();
128 | }
129 | }
130 | ```
131 | 3. Register the hook as extension in `phpunit.xml.dist`
132 | ```xml
133 |
134 |
135 |
136 | ```
137 |
138 | Here you have registered extension that will be called every time when you run `./vendor/bin/phpunit`.
139 |
140 | By default, all functions will be generated and saved into `/vendor/bin/xepozz/internal-mocker/data/mocks.php` file.
141 |
142 | Override the first argument of the `Mocker` constructor to change the path:
143 |
144 | ```php
145 | $mocker = new Mocker('/path/to/your/mocks.php');
146 | ```
147 |
148 | ### Register mocks
149 |
150 | The package supports a few ways to mock functions:
151 |
152 | 1. Runtime mocks
153 | 2. Pre-defined mocks
154 | 3. Mix of two previous ways
155 |
156 | #### Runtime mocks
157 |
158 | If you want to make your test case to be used with mocked function you should register it before.
159 |
160 | Back to the created `MockerExtension::executeBeforeFirstTest` and edit the `$mocks` variable.
161 |
162 | ```php
163 | $mocks = [
164 | [
165 | 'namespace' => 'App\Service',
166 | 'name' => 'time',
167 | ],
168 | ];
169 | ```
170 |
171 | This mock will proxy every call of `time()` under the namespace `App\Service` through a generated wrapper.
172 |
173 | When you want to mock result in tests you should write the following code into needed test case:
174 |
175 | ```php
176 | MockerState::addCondition(
177 | 'App\Service', // namespace
178 | 'time', // function name
179 | [], // arguments
180 | 100 // result
181 | );
182 | ```
183 |
184 | You may also use a callback to set the result of the function:
185 |
186 | ```php
187 | MockerState::addCondition(
188 | '', // namespace
189 | 'headers_sent', // function name
190 | [null, null], // both arguments are references and they are not initialized yet on the function call
191 | fn (&$file, &$line) => $file = $line = 123, // callback result
192 | );
193 | ```
194 |
195 | So your test case will look like the following:
196 |
197 | ```php
198 | assertEquals(100, $service->doSomething());
218 | }
219 | }
220 | ```
221 |
222 | See full example
223 | in [`\Xepozz\InternalMocker\Tests\Integration\DateTimeTest::testRun2`](tests/Integration/DateTimeTest.php)
224 |
225 | #### Pre-defined mock
226 |
227 | Pre-defined mocks allow you to mock behaviour globally.
228 |
229 | It means that you don't need to write `MockerState::addCondition(...)` into each test case if you want to mock it for
230 | whole project.
231 |
232 | > Keep in mind that the same functions from different namespaces are not the same for `Mocker`.
233 |
234 | So back to the created `MockerExtension::executeBeforeFirstTest` and edit the `$mocks` variable.
235 |
236 | ```php
237 | $mocks = [
238 | [
239 | 'namespace' => 'App\Service',
240 | 'name' => 'time',
241 | 'result' => 150,
242 | 'arguments' => [],
243 | ],
244 | ];
245 | ```
246 |
247 | After this variant each `App\Service\time()` will return `150`.
248 |
249 | You can add a lot of mocks. `Mocker` compares the `arguments` values with arguments of calling function and returns
250 | needed result.
251 |
252 | #### Mix of two previous ways
253 |
254 | Mix means that you can use **_Pre-defined mock_** at first and **_Runtime mock_** after.
255 |
256 | ### State
257 |
258 | If you use `Runtime mock` you may face the problem that after mocking function you still have it mocked in another test
259 | cases.
260 |
261 | `MockerState::saveState()` and `MockerState::resetState()` solves this problem.
262 |
263 | These methods save "current" state and unload each `Runtime mock` mock that was applied.
264 |
265 | Using `MockerState::saveState()` after `Mocker->load($mocks)` saves only **_Pre-defined_** mocks.
266 |
267 | ### Tracking calls
268 |
269 | You may track calls of mocked functions by using `MockerState::getTraces()` method.
270 |
271 | ```php
272 | $traces = MockerState::getTraces('App\Service', 'time');
273 | ```
274 |
275 | `$traces` will contain an array of arrays with the following structure:
276 |
277 | ```php
278 | [
279 | [
280 | 'arguments' => [], // arguments of the function
281 | 'trace' => [], // the result of debug_backtrace function
282 | 'result' => 1708764835, // result of the function
283 | ],
284 | // ...
285 | ]
286 | ```
287 |
288 | ### Function signature stubs
289 |
290 | All internal functions are stubbed to be compatible with the original ones.
291 | It makes the functions use referenced arguments (`&$file`) as the originals do.
292 |
293 | They are located in the [`src/stubs.php`](src/stubs.php) file.
294 |
295 | If you need to add a new function signature, override the second argument of the `Mocker` constructor:
296 |
297 | ```php
298 | $mocker = new Mocker(stubPath: '/path/to/your/stubs.php');
299 | ```
300 |
301 | ## Global namespaced functions
302 |
303 | ### Internal functions
304 |
305 | The way you can mock global functions is to disable them
306 | in `php.ini`: https://www.php.net/manual/en/ini.core.php#ini.disable-functions
307 |
308 | The best way is to disable them only for tests by running a command with the additional flags:
309 |
310 | ```bash
311 | php -ddisable_functions=${functions} ./vendor/bin/phpunit
312 | ```
313 |
314 | > If you are using PHPStorm you may set the command in the `Run/Debug Configurations` section.
315 | > Add the flag `-ddisable_functions=${functions}` to the `Interpreter options` field.
316 |
317 | > You may keep the command in the `composer.json` file under the `scripts` section.
318 |
319 | ```json
320 | {
321 | "scripts": {
322 | "test": "php -ddisable_functions=time,serialize,header,date ./vendor/bin/phpunit"
323 | }
324 | }
325 | ```
326 |
327 | > Replace `${functions}` with the list of functions that you want to mock, separated by commas, e.g.: `time,rand`.
328 |
329 | So now you can mock global functions as well.
330 |
331 | #### Internal function implementation
332 |
333 | When you disable a function in `php.ini` you cannot call it anymore. That means you must implement it by yourself.
334 |
335 | Obviously, almost all functions are implemented in PHP looks the same as the Bash ones.
336 |
337 | The shortest way to implement a function is to use ``` `bash command` ``` syntax:
338 |
339 | ```php
340 | $mocks[] = [
341 | 'namespace' => '',
342 | 'name' => 'time',
343 | 'function' => fn () => `date +%s`,
344 | ];
345 | ```
346 |
347 | > Keep in mind that leaving a global function without implementation will cause a recourse call of the function,
348 | > that will lead to a fatal error.
349 |
350 | ## Restrictions
351 |
352 | ### Data Providers
353 |
354 | Sometimes you may face unpleasant situation when mocked function is not mocking without forced using `namespace`
355 | + `function`.
356 | It may mean that you are trying make PHP interpreter file in `@dataProvider`.
357 | Be careful of it and as a workaround I may suggest you to call the mocker in test's constructor.
358 | So first move all code from your extension method `executeBeforeFirstTest` to new static method
359 | and call it in both `executeBeforeFirstTest` and `__construct` methods.
360 |
361 | ```php
362 | final class MyTest extends \PHPUnit\Framework\TestCase
363 | {
364 | public function __construct(?string $name = null, array $data = [], $dataName = '')
365 | {
366 | \App\Tests\MockerExtension::load();
367 | parent::__construct($name, $data, $dataName);
368 | }
369 |
370 | /// ...
371 | }
372 | ```
373 |
374 | ```php
375 | final class MockerExtension implements BeforeTestHook, BeforeFirstTestHook
376 | {
377 | public function executeBeforeFirstTest(): void
378 | {
379 | self::load();
380 | }
381 |
382 | public static function load(): void
383 | {
384 | $mocks = [];
385 |
386 | $mocker = new Mocker();
387 | $mocker->load($mocks);
388 | MockerState::saveState();
389 | }
390 |
391 | public function executeBeforeTest(string $test): void
392 | {
393 | MockerState::resetState();
394 | }
395 | }
396 | ```
397 |
398 | That all because of PHPUnit 9.5 and lower event management system.
399 | Data Provider functionality starts to work before any events, so it's impossible to mock the function at the beginning
400 | of
401 | the runtime.
402 |
--------------------------------------------------------------------------------