├── .gitignore ├── .travis.yml ├── README.md ├── build └── .gitkeep ├── composer.json ├── phpunit.xml.dist ├── src └── PHPUnit │ └── Extension │ ├── FunctionMocker.php │ └── FunctionMocker │ └── CodeGenerator.php └── tests └── PHPUnitTests └── Extension ├── Fixtures └── TestClass.php ├── FunctionMocker └── CodeGeneratorTest.php ├── FunctionMockerTest.php └── IntegrationTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .idea 3 | atlassian-ide-plugin.xml 4 | composer.lock 5 | build/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | sudo: false 3 | php: 4 | - 7.1 5 | - 7.2 6 | - 7.3 7 | - 7.4 8 | - nightly 9 | 10 | matrix: 11 | allow_failures: 12 | - php: nightly 13 | 14 | before_script: 15 | - composer install 16 | 17 | script: 18 | - vendor/bin/phpunit 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHPUnit function mocker extension 2 | 3 | Allows mocking otherwise untestable PHP functions through the use of namespaces. 4 | 5 | [![Build Status](https://secure.travis-ci.org/lstrojny/phpunit-function-mocker.svg)](http://travis-ci.org/lstrojny/phpunit-function-mocker) 6 | 7 | ```php 8 | 0 && ctype_alpha($string); 16 | } 17 | } 18 | ``` 19 | 20 | ```php 21 | php = PHPUnit_Extension_FunctionMocker::start($this, 'MyNamespace') 30 | ->mockFunction('strlen') 31 | ->mockFunction('ctype_alpha') 32 | ->getMock(); 33 | } 34 | 35 | /** @runInSeparateProcess */ 36 | public function testIsStringUsesStrlenAndCtypeAlpha() 37 | { 38 | $this->php 39 | ->expects($this->once()) 40 | ->method('strlen') 41 | ->with('foo') 42 | ->will($this->returnValue(3)) 43 | ; 44 | $this->php 45 | ->expects($this->once()) 46 | ->method('ctype_alpha') 47 | ->with('foo') 48 | ->will($this->returnValue(false)) 49 | ; 50 | 51 | $tool = new MyNamespace\Tool(); 52 | $this->assertFalse($tool->isString('foo')); 53 | } 54 | } 55 | ``` 56 | ### NOTE 57 | Use `@runInSeparateProcess` annotation to make sure that the mocking is reliably working in PHP >=5.4 58 | -------------------------------------------------------------------------------- /build/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lstrojny/phpunit-function-mocker/8934e826f7dc6b91c387f52e704c859c6cf0695f/build/.gitkeep -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lstrojny/phpunit-function-mocker", 3 | "description": "Allows mocking otherwise untestable PHP functions through the use of namespaces", 4 | "license": "MIT", 5 | "require": { 6 | "php": "~7" 7 | }, 8 | "require-dev": { 9 | "phpunit/phpunit": "~6" 10 | }, 11 | "authors": [ 12 | { 13 | "name": "Lars Strojny", 14 | "email": "lstrojny@php.net" 15 | } 16 | ], 17 | "autoload": { 18 | "psr-4": {"PHPUnit\\Extension\\": "src/PHPUnit/Extension"} 19 | }, 20 | "autoload-dev": { 21 | "psr-4": {"PHPUnitTests\\Extension\\": "tests/"} 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | tests/ 29 | vendor/ 30 | /usr/share/php 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/PHPUnit/Extension/FunctionMocker.php: -------------------------------------------------------------------------------- 1 | testCase = $testCase; 24 | $this->namespace = trim($namespace, '\\'); 25 | } 26 | 27 | /** 28 | * Create a mock for the given namespace to override global namespace functions. 29 | * 30 | * Example: PHP global namespace function setcookie() needs to be overridden in order to test 31 | * if a cookie gets set. When setcookie() is called from inside a class in the namespace 32 | * \Foo\Bar the mock setcookie() created here will be used instead to the real function. 33 | * 34 | * @param TestCase $testCase 35 | * @param string $namespace 36 | * @return FunctionMocker 37 | */ 38 | public static function start(TestCase $testCase, $namespace) 39 | { 40 | return new static($testCase, $namespace); 41 | } 42 | 43 | public static function tearDown() 44 | { 45 | unset($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER']); 46 | } 47 | 48 | public function mockFunction($function) 49 | { 50 | $function = trim(strtolower($function)); 51 | 52 | if (!in_array($function, $this->functions, true)) { 53 | $this->functions[] = $function; 54 | } 55 | 56 | return $this; 57 | } 58 | 59 | public function getMock() 60 | { 61 | $mock = $this->testCase->getMockBuilder('stdClass') 62 | ->setMethods($this->functions) 63 | ->setMockClassName('PHPUnit_Extension_FunctionMocker_' . uniqid()) 64 | ->getMock(); 65 | 66 | foreach ($this->functions as $function) { 67 | $fqFunction = $this->namespace . '\\' . $function; 68 | if (in_array($fqFunction, static::$mockedFunctions, true)) { 69 | continue; 70 | } 71 | 72 | if (!extension_loaded('runkit') || !ini_get('runkit.internal_override')) { 73 | CodeGenerator::defineFunction($function, $this->namespace); 74 | } elseif (!function_exists('__phpunit_function_mocker_' . $function)) { 75 | runkit_function_rename($function, '__phpunit_function_mocker_' . $function); 76 | error_log($function); 77 | runkit_method_redefine( 78 | $function, 79 | function () use ($function) { 80 | if (!isset($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER'][$this->namespace])) { 81 | return call_user_func_array('__phpunit_function_mocker_' . $function, func_get_args()); 82 | } 83 | 84 | return call_user_func_array( 85 | array($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER'][$this->namespace], $function), 86 | func_get_args() 87 | ); 88 | } 89 | ); 90 | var_dump(strlen("foo")); 91 | } 92 | 93 | static::$mockedFunctions[] = $fqFunction; 94 | } 95 | 96 | if (!isset($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER'])) { 97 | $GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER'] = array(); 98 | } 99 | 100 | $GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER'][$this->namespace] = $mock; 101 | 102 | return $mock; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/PHPUnit/Extension/FunctionMocker/CodeGenerator.php: -------------------------------------------------------------------------------- 1 | assertEquals($expected, $code); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/PHPUnitTests/Extension/FunctionMockerTest.php: -------------------------------------------------------------------------------- 1 | functionMocker = FunctionMocker::start($this, 'My\TestNamespace'); 17 | } 18 | 19 | public function tearDown() 20 | { 21 | FunctionMocker::tearDown(); 22 | } 23 | 24 | public function testBasicMockingFunction() 25 | { 26 | $this->assertMockFunctionNotDefined('My\TestNamespace\strlen'); 27 | 28 | $this->functionMocker 29 | ->mockFunction('strlen') 30 | ->mockFunction('substr'); 31 | 32 | $this->assertMockFunctionNotDefined('My\TestNamespace\strlen'); 33 | $this->assertMockFunctionNotDefined('My\TestNamespace\substr'); 34 | 35 | $mock = $this->functionMocker->getMock(); 36 | 37 | $this->assertMockFunctionDefined('My\TestNamespace\strlen', 'My\TestNamespace'); 38 | $this->assertMockFunctionDefined('My\TestNamespace\substr', 'My\TestNamespace'); 39 | 40 | $mock 41 | ->expects($this->once()) 42 | ->method('strlen') 43 | ->will($this->returnValue('mocked strlen()')) 44 | ; 45 | $mock 46 | ->expects($this->once()) 47 | ->method('substr') 48 | ->will($this->returnCallback( 49 | function() { 50 | return func_get_args(); 51 | } 52 | )) 53 | ; 54 | 55 | $this->assertMockObjectPresent('My\TestNamespace', $mock); 56 | $this->assertSame('mocked strlen()', \My\TestNamespace\strlen('foo')); 57 | $this->assertSame(array('foo', 0, 3), \My\TestNamespace\substr('foo', 0, 3)); 58 | } 59 | 60 | public function testNamespaceLeadingAndTrailingSlash() 61 | { 62 | $this->functionMocker = FunctionMocker::start($this, '\My\TestNamespace\\'); 63 | 64 | $this->assertMockFunctionNotDefined('My\TestNamespace\strpos'); 65 | 66 | $this->functionMocker 67 | ->mockFunction('strpos'); 68 | 69 | $this->assertMockFunctionNotDefined('My\TestNamespace\strpos'); 70 | 71 | $mock = $this->functionMocker->getMock(); 72 | 73 | $this->assertMockFunctionDefined('My\TestNamespace\strpos', 'My\TestNamespace'); 74 | 75 | $mock 76 | ->expects($this->once()) 77 | ->method('strpos') 78 | ->will($this->returnArgument(1)) 79 | ; 80 | 81 | $this->assertMockObjectPresent('My\TestNamespace', $mock); 82 | $this->assertSame('b', \My\TestNamespace\strpos('abc', 'b')); 83 | } 84 | 85 | public function testFunctionsAreUsedLowercase() 86 | { 87 | $this->assertMockFunctionNotDefined('My\TestNamespace\myfunc'); 88 | 89 | $this->functionMocker 90 | ->mockFunction('myfunc') 91 | ->mockFunction(' myfunc ') 92 | ->mockFunction('MYFUNC'); 93 | 94 | $this->assertMockFunctionNotDefined('My\TestNamespace\myfunc'); 95 | 96 | $mock = $this->functionMocker->getMock(); 97 | 98 | $this->assertMockFunctionDefined('My\TestNamespace\myfunc', 'My\TestNamespace'); 99 | 100 | $mock 101 | ->expects($this->once()) 102 | ->method('myfunc') 103 | ->will($this->returnArgument(0)) 104 | ; 105 | 106 | $this->assertMockObjectPresent('My\TestNamespace', $mock); 107 | $this->assertSame('abc', \My\TestNamespace\myfunc('abc')); 108 | } 109 | 110 | public function testUseOneFunctionMockerMoreThanOnce() 111 | { 112 | $this->assertMockFunctionNotDefined('My\TestNamespace\strtr'); 113 | 114 | $this->functionMocker 115 | ->mockFunction('strtr'); 116 | 117 | $this->assertMockFunctionNotDefined('My\TestNamespace\strtr'); 118 | 119 | $this->functionMocker->getMock(); 120 | 121 | $this->functionMocker 122 | ->mockFunction('strtr'); 123 | 124 | $mock = $this->functionMocker->getMock(); 125 | 126 | $this->assertMockFunctionDefined('My\TestNamespace\strtr', 'My\TestNamespace'); 127 | 128 | $mock 129 | ->expects($this->once()) 130 | ->method('strtr') 131 | ->with('abcd') 132 | ->will($this->returnArgument(0)) 133 | ; 134 | 135 | $this->assertMockObjectPresent('My\TestNamespace', $mock); 136 | 137 | try { 138 | $this->assertSame('abc', \My\TestNamespace\strtr('abc')); 139 | $this->fail('Expected exception'); 140 | } catch (AssertionFailedError $e) { 141 | $this->assertContains('does not match expected value', $e->getMessage()); 142 | } 143 | 144 | /** Reset mock objects */ 145 | $reflected = new ReflectionClass(TestCase::class); 146 | $mockObjects = $reflected->getProperty('mockObjects'); 147 | $mockObjects->setAccessible(true); 148 | $mockObjects->setValue($this, array()); 149 | } 150 | 151 | public function testMockSameFunctionIsDifferentNamespaces() 152 | { 153 | $this->assertMockFunctionNotDefined('My\TestNamespace\foofunc'); 154 | $this->functionMocker 155 | ->mockFunction('foofunc'); 156 | $this->assertMockFunctionNotDefined('My\TestNamespace\foofunc'); 157 | $this->functionMocker->getMock(); 158 | $this->assertMockFunctionDefined('My\TestNamespace\foofunc', 'My\TestNamespace'); 159 | 160 | $this->functionMocker = FunctionMocker::start($this, 'My\TestNamespace2'); 161 | $this->assertFalse(function_exists('My\TestNamespace2\foofunc')); 162 | $this->functionMocker 163 | ->mockFunction('foofunc'); 164 | $this->assertFalse(function_exists('My\TestNamespace2\foofunc')); 165 | $this->functionMocker->getMock(); 166 | $this->assertMockFunctionDefined('My\TestNamespace2\foofunc', 'My\TestNamespace2'); 167 | } 168 | 169 | public function assertMockFunctionNotDefined($function) 170 | { 171 | $this->assertFalse( 172 | function_exists($function), 173 | sprintf('Function "%s()" was expected to be undefined', $function) 174 | ); 175 | $this->assertArrayNotHasKey('__PHPUNIT_EXTENSION_FUNCTIONMOCKER', $GLOBALS); 176 | } 177 | 178 | public function assertMockFunctionDefined($function, $namespace) 179 | { 180 | $this->assertTrue(function_exists($function)); 181 | $this->assertArrayHasKey('__PHPUNIT_EXTENSION_FUNCTIONMOCKER', $GLOBALS); 182 | $this->assertArrayHasKey($namespace, $GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER']); 183 | } 184 | 185 | public function assertMockObjectPresent($namespace, $mock) 186 | { 187 | $this->assertArrayHasKey('__PHPUNIT_EXTENSION_FUNCTIONMOCKER', $GLOBALS); 188 | $this->assertArrayHasKey($namespace, $GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER']); 189 | $this->assertSame($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER'][$namespace], $mock); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /tests/PHPUnitTests/Extension/IntegrationTest.php: -------------------------------------------------------------------------------- 1 | php = FunctionMocker::start($this, 'PHPUnitTests\Extension\Fixtures') 16 | ->mockFunction('strpos') 17 | ->getMock(); 18 | } 19 | 20 | public function testMocked() 21 | { 22 | $this->php 23 | ->expects($this->once()) 24 | ->method('strpos') 25 | ->with('ffoo', 'o') 26 | ->will($this->returnValue('mocked')); 27 | 28 | $this->assertSame('mocked', \PHPUnitTests\Extension\Fixtures\TestClass::invokeGlobalFunction()); 29 | } 30 | 31 | public function testMockingGlobalFunctionAndCallingOriginalAgain() 32 | { 33 | $this->testMocked(); 34 | FunctionMocker::tearDown(); 35 | $this->assertSame(2, \PHPUnitTests\Extension\Fixtures\TestClass::invokeGlobalFunction()); 36 | } 37 | } 38 | --------------------------------------------------------------------------------