From e3581a9d52283b8b03c552445e4ab6ac09200a09 Mon Sep 17 00:00:00 2001 From: Dmitrii Derepko Date: Sat, 24 Feb 2024 15:51:18 +0700 Subject: [PATCH 1/2] Add tracing mocked function --- README.md | 30 ++++++++++++ composer.json | 3 ++ src/Mocker.php | 16 ++++--- src/MockerState.php | 36 +++++++++++++- tests/Integration/TraceTest.php | 84 +++++++++++++++++++++++++++++++++ tests/MockerTest.php | 22 ++++++--- 6 files changed, 177 insertions(+), 14 deletions(-) create mode 100644 tests/Integration/TraceTest.php diff --git a/README.md b/README.md index dcff51e..22e3c43 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,23 @@ functions as: `time()`, `str_contains()`, `rand`, etc. [![Total Downloads](https://poser.pugx.org/xepozz/internal-mocker/downloads.svg)](https://packagist.org/packages/xepozz/internal-mocker) [![phpunit](https://github.com/xepozz/internal-mocker/workflows/PHPUnit/badge.svg)](https://github.com/xepozz/internal-mocker/actions) +# Table of contents + +- [Installation](#installation) +- [Usage](#usage) + - [Register a hook](#register-a-hook) + - [Register mocks](#register-mocks) + - [Runtime mocks](#runtime-mocks) + - [Pre-defined mock](#pre-defined-mock) + - [Mix of two previous ways](#mix-of-two-previous-ways) + - [State](#state) +- [Global namespaced functions](#global-namespaced-functions) + - [Internal functions](#internal-functions) + - [Workaround](#workaround) + - [Internal function implementation](#internal-function-implementation) +- [Restrictions](#restrictions) + - [Data Providers](#data-providers) + ## Installation ```bash @@ -186,6 +203,19 @@ The best way is to disable them only for tests by running a command with the add php -ddisable_functions=${functions} ./vendor/bin/phpunit ``` +> If you are using PHPStorm you may set the command in the `Run/Debug Configurations` section. +> Add the flag `-ddisable_functions=${functions}` to the `Interpreter options` field. + +> You may keep the command in the `composer.json` file under the `scripts` section. + +```json +{ + "scripts": { + "test": "php -ddisable_functions=time,serialize,header,date ./vendor/bin/phpunit" + } +} +``` + > Replace `${functions}` with the list of functions that you want to mock, separated by commas, e.g.: `time,rand`. So now you can mock global functions as well. diff --git a/composer.json b/composer.json index 4f700ed..2d65638 100644 --- a/composer.json +++ b/composer.json @@ -22,5 +22,8 @@ "psr-4": { "Xepozz\\InternalMocker\\Tests\\": "tests/" } + }, + "scripts": { + "test": "php -ddisable_functions=time vendor/bin/phpunit" } } diff --git a/src/Mocker.php b/src/Mocker.php index aecd77a..73935b5 100644 --- a/src/Mocker.php +++ b/src/Mocker.php @@ -42,7 +42,7 @@ public function generate(array $mocks): string $defaultString = $imock['default'] ? 'true' : 'false'; $mockerConfig[] = <<path; } -} \ No newline at end of file +} diff --git a/src/MockerState.php b/src/MockerState.php index 23ff5d3..8dba420 100644 --- a/src/MockerState.php +++ b/src/MockerState.php @@ -6,6 +6,7 @@ final class MockerState { + private static mixed $traces = []; private static array $savedState = []; private static array $state = []; private static array $defaults = []; @@ -106,5 +107,38 @@ public static function saveState(): void public static function resetState(): void { self::$state = self::$savedState; + self::$traces = []; } -} \ No newline at end of file + + public static function saveTrace( + string $namespace, + string $functionName, + array $arguments + ): int { + $position = count(self::$traces[$namespace][$functionName] ?? []); + self::$traces[$namespace][$functionName][$position] = [ + 'arguments' => $arguments, + 'trace' => debug_backtrace(), + ]; + + return $position; + } + + public static function saveTraceResult( + string $namespace, + string $functionName, + int $position, + mixed $result + ): mixed { + self::$traces[$namespace][$functionName][$position]['result'] = $result; + + return $result; + } + + public static function getTraces( + string $namespace, + string $functionName + ): array { + return self::$traces[$namespace][$functionName] ?? []; + } +} diff --git a/tests/Integration/TraceTest.php b/tests/Integration/TraceTest.php new file mode 100644 index 0000000..99e990b --- /dev/null +++ b/tests/Integration/TraceTest.php @@ -0,0 +1,84 @@ +run('test'); + $object->run('test2'); + $object->run('test3'); + + $traces = MockerState::getTraces( + __NAMESPACE__, + 'serialize', + ); + + $this->assertIsArray($traces); + + $this->assertCount(3, $traces); + + foreach ($traces as $trace) { + $this->assertArrayHasKey('arguments', $trace); + $this->assertArrayHasKey('result', $trace); + + $this->assertIsArray($trace['arguments']); + $this->assertIsArray($trace['trace']); + $this->assertIsString($trace['result']); + } + + $this->assertEquals(['test'], $traces[0]['arguments']); + $this->assertEquals('s:4:"test";', $traces[0]['result']); + + $this->assertEquals(['test2'], $traces[1]['arguments']); + $this->assertEquals('s:5:"test2";', $traces[1]['result']); + + $this->assertEquals(['test3'], $traces[2]['arguments']); + $this->assertEquals('s:5:"test3";', $traces[2]['result']); + } + + public function testBacktrace(): void + { + $object = new UseInSucceedDataProviderStub(); + $object->run('test'); + + $traces = MockerState::getTraces( + __NAMESPACE__, + 'serialize', + ); + + $this->assertIsArray($traces); + + $this->assertCount(1, $traces); + + $this->assertEquals(['test'], $traces[0]['arguments']); + $this->assertEquals('saveTrace', $traces[0]['trace'][0]['function']); + $this->assertEquals(120, $traces[0]['trace'][0]['line']); + $this->assertEquals( + [ + __NAMESPACE__, + 'serialize', + ['test'], + ], + $traces[0]['trace'][0]['args'], + ); + + $this->assertEquals(__NAMESPACE__ . '\serialize', $traces[0]['trace'][1]['function']); + $this->assertEquals(11, $traces[0]['trace'][1]['line']); + $this->assertEquals(['test'], $traces[0]['trace'][1]['args']); + } +} \ No newline at end of file diff --git a/tests/MockerTest.php b/tests/MockerTest.php index cf40967..f5e4acb 100644 --- a/tests/MockerTest.php +++ b/tests/MockerTest.php @@ -35,7 +35,7 @@ public function generateProvider() use Xepozz\InternalMocker\MockerState; MockerState::addCondition( - "Xepozz\InternalMocker\Tests\Integration", + "Xepozz\InternalMocker\Tests\Integration", "time", [], 555, @@ -49,10 +49,14 @@ public function generateProvider() function time(...\$arguments) { + \$position = MockerState::saveTrace(__NAMESPACE__, "time", \$arguments); if (MockerState::checkCondition(__NAMESPACE__, "time", \$arguments)) { - return MockerState::getResult(__NAMESPACE__, "time", \$arguments); + \$result = MockerState::getResult(__NAMESPACE__, "time", \$arguments); + } else { + \$result = MockerState::getDefaultResult(__NAMESPACE__, "time", fn() => \\time(...\$arguments)); } - return MockerState::getDefaultResult(__NAMESPACE__, "time", fn() => \\time(...\$arguments)); + + return MockerState::saveTraceResult(__NAMESPACE__, "time", \$position, \$result); } } PHP, @@ -83,14 +87,14 @@ function time(...\$arguments) use Xepozz\InternalMocker\MockerState; MockerState::addCondition( - "Xepozz\InternalMocker\Tests\Integration", + "Xepozz\InternalMocker\Tests\Integration", "str_contains", ['haystack' => 'string','needle' => 'str'], false, false, ); MockerState::addCondition( - "Xepozz\InternalMocker\Tests\Integration", + "Xepozz\InternalMocker\Tests\Integration", "str_contains", ['haystack' => 'string2','needle' => 'str'], false, @@ -104,10 +108,14 @@ function time(...\$arguments) function str_contains(...\$arguments) { + \$position = MockerState::saveTrace(__NAMESPACE__, "str_contains", \$arguments); if (MockerState::checkCondition(__NAMESPACE__, "str_contains", \$arguments)) { - return MockerState::getResult(__NAMESPACE__, "str_contains", \$arguments); + \$result = MockerState::getResult(__NAMESPACE__, "str_contains", \$arguments); + } else { + \$result = MockerState::getDefaultResult(__NAMESPACE__, "str_contains", fn() => \\str_contains(...\$arguments)); } - return MockerState::getDefaultResult(__NAMESPACE__, "str_contains", fn() => \\str_contains(...\$arguments)); + + return MockerState::saveTraceResult(__NAMESPACE__, "str_contains", \$position, \$result); } } PHP, From be8669e4bf1c104221f1328d962893c5b2f92649 Mon Sep 17 00:00:00 2001 From: Dmitrii Derepko Date: Sat, 24 Feb 2024 15:54:24 +0700 Subject: [PATCH 2/2] Add doc --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 22e3c43..55c0c9d 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ functions as: `time()`, `str_contains()`, `rand`, etc. - [Pre-defined mock](#pre-defined-mock) - [Mix of two previous ways](#mix-of-two-previous-ways) - [State](#state) + - [Tracking calls](#tracking-calls) - [Global namespaced functions](#global-namespaced-functions) - [Internal functions](#internal-functions) - [Workaround](#workaround) @@ -183,6 +184,27 @@ 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. +### Tracking calls + +You may track calls of mocked functions by using `MockerState::getTraces()` method. + +```php +$traces = MockerState::getTraces('App\Service', 'time'); +``` + +`$traces` will contain an array of arrays with the following structure: + +```php +[ + [ + 'arguments' => [], // arguments of the function + 'trace' => [], // the result of debug_backtrace function + 'result' => 1708764835, // result of the function + ], + // ... +] +``` + ## Global namespaced functions ### Internal functions