├── 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 | [![Latest Stable Version](https://poser.pugx.org/xepozz/internal-mocker/v/stable.svg)](https://packagist.org/packages/xepozz/internal-mocker) 7 | [![Total Downloads](https://poser.pugx.org/xepozz/internal-mocker/downloads.svg)](https://packagist.org/packages/xepozz/internal-mocker) 8 | [![phpunit](https://github.com/xepozz/internal-mocker/workflows/PHPUnit/badge.svg)](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 | --------------------------------------------------------------------------------