├── .travis.yml ├── PHPUnit └── Extensions │ ├── MockFunction.php │ └── MockStaticMethod.php ├── README.md ├── Tests └── Extensions │ ├── MockFunctionTest.php │ └── MockStaticMethodTest.php ├── composer.json └── phpunit.xml /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - "5.6" 4 | - "5.5" 5 | - "5.4" 6 | - "5.3" 7 | before_script: 8 | - git clone https://github.com/zenovich/runkit.git runkit 9 | - cd runkit 10 | - phpize 11 | - ./configure 12 | - make 13 | - sudo make install 14 | - echo "extension=runkit.so" >> `php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||"` 15 | - cd .. 16 | script: phpunit --coverage-text --configuration=phpunit.xml -------------------------------------------------------------------------------- /PHPUnit/Extensions/MockFunction.php: -------------------------------------------------------------------------------- 1 | id = self::$next_id; 114 | $this->function_name = $function_name; 115 | $this->scope_object = $scope_object; 116 | $this->test_case = self::findTestCase(); 117 | $this->mock_object = 118 | $this->test_case->getMockBuilder( 119 | 'Mock_' . str_replace( '::', '__', $this->function_name ) . '_' . $this->id 120 | ) 121 | ->disableAutoload() 122 | ->setMethods(array( 'invoked' )) 123 | ->getMock(); 124 | 125 | ++self::$next_id; 126 | self::$instances[$this->id] = $this; 127 | 128 | $this->createFunction(); 129 | } 130 | 131 | /** 132 | * Called when all the referneces to the object are removed (even self::$instances). 133 | * 134 | * Makes sure the replaced functions are finally cleared in case runkit 135 | * "forgets" to remove them in the end of the request. 136 | * It is still highly recommended to call restore() explicitly! 137 | */ 138 | public function __destruct() 139 | { 140 | $this->restore(); 141 | } 142 | 143 | /** 144 | * Clean-up function. 145 | * 146 | * Removes mocked function and restored the original was there is any. 147 | * Also removes the reference to the object from self::$instances. 148 | */ 149 | public function restore() 150 | { 151 | if ( $this->active ) 152 | { 153 | runkit_function_remove( $this->function_name ); 154 | if ( isset( $this->restore_name ) ) 155 | { 156 | runkit_function_rename( $this->restore_name, $this->function_name ); 157 | } 158 | $this->active = false; 159 | } 160 | 161 | if ( isset( self::$instances[$this->id] ) ) 162 | { 163 | unset( self::$instances[$this->id] ); 164 | } 165 | } 166 | 167 | /** 168 | * Callback method to be used in runkit function when it is invoked. 169 | * 170 | * It takes the parameters of the function call and passes them to the mock object. 171 | * 172 | * @param type $arguments 0-indexed array of arguments with which the mocked function was called. 173 | * @return mixed 174 | */ 175 | public function invoked( array $arguments ) 176 | { 177 | // Original function is called when the invocation is ousides he scope or 178 | // the invocation comes from this object. 179 | $caller_object = self::getCallStackObject( self::CALL_STACK_DISTANCE ); 180 | if ( $caller_object === $this || ( isset( $this->scope_object ) && $this->scope_object !== $caller_object ) ) 181 | { 182 | if ( isset( $this->restore_name ) ) 183 | { 184 | return $this->callOriginal( $arguments ); 185 | } 186 | trigger_error( 'Undefined function: ' . $this->function_name, E_USER_ERROR ); 187 | } 188 | return call_user_func_array( array( $this->mock_object, __FUNCTION__ ), $arguments ); 189 | } 190 | 191 | /** 192 | * Calls original function that we temporary renamed. This maintains the oriignal functionality. 193 | * 194 | * @param type $arguments 195 | * @return mixed 196 | */ 197 | protected function callOriginal( array $arguments ) 198 | { 199 | return call_user_func_array( $this->restore_name, $arguments ); 200 | } 201 | 202 | /** 203 | * Proxy to the 'expects' of the mock object. 204 | * 205 | * Also calld method() so after this the mock object can be used to set 206 | * parameter constraints and return values. 207 | * 208 | * @return object 209 | */ 210 | public function expects() 211 | { 212 | $arguments = func_get_args(); 213 | return call_user_func_array( array( $this->mock_object, __FUNCTION__ ), $arguments )->method( 'invoked' ); 214 | } 215 | 216 | /** 217 | * Returns an instance of this class selected by its ID. Used in the runkit function. 218 | * 219 | * @param integer $id 220 | * @return object 221 | */ 222 | public static function findMock( $id ) 223 | { 224 | if ( !isset( self::$instances[$id] ) ) 225 | { 226 | throw new Exception( 'Mock object not found, might be destroyed already.' ); 227 | } 228 | return self::$instances[$id]; 229 | } 230 | 231 | /** 232 | * Finds the rist object in the call cstack that is instance of a PHPUnit test case. 233 | * 234 | * @see self::TESTCASE_CLASSNAME 235 | * @return PHPUnit_Framework_TestCase 236 | */ 237 | public static function findTestCase() 238 | { 239 | $backtrace = debug_backtrace(); 240 | $classname = self::TESTCASE_CLASSNAME; 241 | 242 | do 243 | { 244 | $calling_test = array_shift( $backtrace ); 245 | } while( isset( $calling_test ) && !( isset( $calling_test['object'] ) && $calling_test['object'] instanceof $classname ) ); 246 | 247 | if ( !isset( $calling_test ) ) 248 | { 249 | trigger_error( 'No calling test found.', E_USER_ERROR ); 250 | } 251 | 252 | return $calling_test['object']; 253 | } 254 | 255 | /** 256 | * Creates runkit function to be used for mocking, taking care of callback to this object. 257 | * 258 | * Also temporary renames the original function if there is. 259 | */ 260 | protected function createFunction() 261 | { 262 | if ( function_exists( $this->function_name ) ) 263 | { 264 | $this->restore_name = 'restore_' . $this->function_name . '_' . $this->id . '_' . uniqid(); 265 | 266 | runkit_function_copy( $this->function_name, $this->restore_name ); 267 | runkit_function_redefine( $this->function_name, '', $this->getCallback() ); 268 | } 269 | else 270 | { 271 | runkit_function_add( $this->function_name, '', $this->getCallback() ); 272 | } 273 | 274 | $this->active = true; 275 | } 276 | 277 | /** 278 | * Gives back the source code body of the runkit function replacing the original. 279 | * 280 | * The function is quite simple - find the function mock instance (of this class) 281 | * that created it, then calls its invoked() method with the parameters of its invokation. 282 | * 283 | * @return string 284 | */ 285 | protected function getCallback() 286 | { 287 | $class_name = __CLASS__; 288 | return <<id} ); 290 | \$arguments = func_get_args(); 291 | return \$mock->invoked( \$arguments ); 292 | CALLBACK; 293 | } 294 | 295 | /** 296 | * Returns an object from the call stack at Nth distance if there is, null otherwise. 297 | * 298 | * In theory we should instement the distance by one because when we call this 299 | * method, we don't count it itself to the callstack, but since the stack is 300 | * 0-indexed, we can avoid this step. 301 | * 302 | * Function calls are ignored, the first call after $distance that is made form 303 | * is returned. 304 | * 305 | * @param type $distance The distance in the call stack from the current call and the desired one. 306 | * @return object 307 | */ 308 | protected static function getCallStackObject( $distance ) 309 | { 310 | $backtrace = debug_backtrace(); 311 | 312 | do 313 | { 314 | if ( isset( $backtrace[$distance]['object'] ) ) 315 | { 316 | return $backtrace[$distance]['object']; 317 | } 318 | 319 | /* If there is no object assiciated to this call, we go further until 320 | * the next one. 321 | * Funcsion calls and functions like "user_call_func" get ignored. 322 | */ 323 | ++$distance; 324 | } while ( isset( $backtrace[$distance] ) ); 325 | 326 | return null; 327 | } 328 | 329 | } 330 | -------------------------------------------------------------------------------- /PHPUnit/Extensions/MockStaticMethod.php: -------------------------------------------------------------------------------- 1 | active ) 21 | { 22 | list( $class, $method ) = $this->getClassAndMethod(); 23 | 24 | runkit_method_remove( $class, $method ); 25 | runkit_method_rename( $class, $this->restore_name, $method ); 26 | $this->active = false; 27 | } 28 | 29 | parent::restore(); 30 | } 31 | 32 | /** 33 | * Calls original method that we temporary renamed. This maintains the oriignal functionality. 34 | * 35 | * @param type $arguments 36 | * @return mixed 37 | */ 38 | protected function callOriginal( array $arguments ) 39 | { 40 | list( $class ) = $this->getClassAndMethod(); 41 | return call_user_func_array( array( $class, $this->restore_name ), $arguments ); 42 | } 43 | 44 | /** 45 | * Creates runkit method to be used for mocking, taking care of callback to this object. 46 | * 47 | * Also temporary renames the original method if there is. 48 | */ 49 | protected function createFunction() 50 | { 51 | list( $class, $method ) = $this->getClassAndMethod(); 52 | 53 | $this->restore_name = 'restore_' . $class . '_' . $method . '_' . $this->id . '_' . uniqid(); 54 | 55 | // We save the original method in the class for restoring. 56 | runkit_method_copy( $class, $this->restore_name, $class, $method ); 57 | runkit_method_redefine( $class, $method, '', $this->getCallback(), RUNKIT_ACC_STATIC ); 58 | 59 | $this->active = true; 60 | } 61 | 62 | /** 63 | * Extracts classname and method name from a string written like Class::method and checks for their existence. 64 | * 65 | * @staticvar array $class_and_method Memoization of the classname and method name. 66 | * @return array Contains classname (0. offset) and method name (1. offset). 67 | */ 68 | protected function getClassAndMethod() 69 | { 70 | static $classes_and_methods = array(); 71 | 72 | if ( isset( $classes_and_methods[$this->function_name] ) ) 73 | { 74 | return $classes_and_methods[$this->function_name]; 75 | } 76 | 77 | $class_and_method = explode( '::', $this->function_name ); 78 | 79 | if ( 2 !== count( $class_and_method ) ) 80 | { 81 | trigger_error( 'Invalid static method name. Please provide Classname::method format.', E_USER_ERROR ); 82 | } 83 | 84 | if ( !class_exists( $class_and_method[0] ) ) 85 | { 86 | trigger_error( "Class '{$class_and_method[0]}' must exist in order the static method to be mocked.", E_USER_ERROR ); 87 | } 88 | 89 | if ( !is_callable( $class_and_method ) ) 90 | { 91 | trigger_error( "Static method '{$this->function_name}' must exist and be public.", E_USER_ERROR ); 92 | } 93 | 94 | return $classes_and_methods[$this->function_name] = $class_and_method; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Introduction ## 2 | 3 | MockFunction is a PHPUnit extension that uses `runkit` to mock PHP functions (both user-defined and system) or static methods and use mockobject-style invocation matchers, parameter constraints and all that magic. 4 | 5 | To use this extension, you have to install `runkit` first (PECL package). For a working version see https://github.com/zenovich/runkit/ 6 | 7 | To be able to mock system function (not user-defined ones), you need to turn on `runkit.internal_override` in the PHP config. 8 | 9 | ## Installation ## 10 | 11 | If you use composer, installing MockFunction is easy: 12 | 13 | "require-dev": { 14 | "tcz/phpunit-mockfunction": "1.0.0" 15 | } 16 | 17 | Then 18 | 19 | php composer.phar update tcz/phpunit-mockfunction 20 | 21 | ## Usage ## 22 | 23 | Assuming you are in a PHPUnit test: 24 | 25 | // Back to the future: 26 | $flux_capacitor = new PHPUnit_Extensions_MockFunction( 'time', $this->object ); 27 | $einsteins_clock = time() + 60; 28 | $flux_capacitor->expects( $this->atLeastOnce() )->will( $this->returnValue( $einsteins_clock ) ); 29 | 30 | Where `$flux_capacitor` is the stub function. It can be set up with the same fluent interface as a `MockObject` (excluding method, of course). 31 | 32 | The 2nd parameter of the constructor (`$this->object`) is the object where we expect the function to be called. The "mocking" only takes effect from here, from all the other sources it will execute the "normal" function (see next line). 33 | 34 | Variable `$einsteins_clock` contains the value that we will return instead of the "regular" value (we add 1 minute for the current time). 35 | 36 | In the next line we set up the mock function with the fluent interface of a mock object. 37 | 38 | The mocked function is active for the test object instance until `$flux_capacitor->restore();` is called. If you happen to forget this in the end of the test case, normally it is not a problem, because you will test anew instance of your tested class with each test case. 39 | 40 | To mock static methods, you can use PHPUnit_Extensions_MockStaticMethod class. It work int the same way as with functions: 41 | 42 | $mocked_static = new PHPUnit_Extensions_MockStaticMethod( 'MyClass::myMethod', $this->object ); 43 | 44 | ## Advanced mocking ## 45 | 46 | You can use all invocation matchers, constraints and stub returns, for example: 47 | 48 | // This will execute the original function at the end, but will test 49 | // the number of exections ( $this->once() ) and the correct parameter ( $this->equalTo() ). 50 | $mocked_strrev = new PHPUnit_Extensions_MockFunction( 'strrev', $this->object ); 51 | $mocked_strrev->expects( $this->once() )->with( $this->equalTo( 'abc' ) )->will( $this->returnCallback( 'strrev' ) ); 52 | 53 | 54 | // This object cannot execute shell_exec. 55 | $mocked_shell = new PHPUnit_Extensions_MockFunction( 'shell_exec', $this->object ); 56 | $mocked_shell->expects( $this->never() ); 57 | 58 | 59 | // Expecting to check the existence of 2 file, returning true for both. 60 | $mocked_file_exists = new PHPUnit_Extensions_MockFunction( 'file_exists', $this->object ); 61 | $mocked_file_exists->expects( $this->exactly( 2 ) ) 62 | ->with( 63 | $this->logicalOr( 64 | $this->equalTo( '/tmp/file1.exe' ), 65 | $this->equalTo( '/tmp/file2.exe' ) 66 | ) 67 | )->will( $this->returnValue( true ) ); 68 | 69 | For further information see http://www.phpunit.de/manual/3.0/en/mock-objects.html 70 | 71 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/tcz/phpunit-mockfunction/trend.png)](https://bitdeli.com/free "Bitdeli Badge") [![Build Status](https://travis-ci.org/tcz/phpunit-mockfunction.svg?branch=master)](https://travis-ci.org/tcz/phpunit-mockfunction) 72 | 73 | -------------------------------------------------------------------------------- /Tests/Extensions/MockFunctionTest.php: -------------------------------------------------------------------------------- 1 | test_scope_object = new TestScopeObject(); 38 | } 39 | 40 | /** 41 | * Test simple function return faking without consraints. 42 | */ 43 | public function testMockWithReturn() 44 | { 45 | $this->test_function_name = self::getFunctionName( 'time' ); 46 | $this->object = new PHPUnit_Extensions_MockFunction( $this->test_function_name, $this->test_scope_object ); 47 | 48 | // Back to the future: 49 | $einsteins_clock = time() + 60; 50 | $this->object->expects( $this->atLeastOnce() )->will( $this->returnValue( $einsteins_clock ) ); 51 | 52 | // Einstein's clock is exactly one minute behind mine. 53 | $this->assertSame( $einsteins_clock, $this->test_scope_object->callFunction( $this->test_function_name, array() ) ); 54 | 55 | $this->object->restore(); 56 | 57 | // We are back in 1985. 58 | $this->assertSame( time(), $this->test_scope_object->callFunction( $this->test_function_name, array() ) ); 59 | } 60 | 61 | /** 62 | * Test more advanced mocking with return callback and constraints. 63 | */ 64 | public function testMockWithOriginal() 65 | { 66 | $this->test_function_name = self::getFunctionName( 'strrev' ); 67 | $this->object = new PHPUnit_Extensions_MockFunction( $this->test_function_name, $this->test_scope_object ); 68 | 69 | // Return normally, only checks the call. 70 | $this->object->expects( $this->once() )->with( $this->equalTo( 'abc' ) )->will( $this->returnCallback( 'strrev' ) ); 71 | 72 | // The same output is returned. 73 | $this->assertSame( 'cba', $this->test_scope_object->callFunction( $this->test_function_name, array( 'abc' ) ) ); 74 | 75 | $this->object->restore(); 76 | } 77 | 78 | /** 79 | * Testing newly created function. 80 | */ 81 | public function testMockNewFunction() 82 | { 83 | $this->test_function_name = 'new_random_function_' . uniqid(); 84 | $this->object = new PHPUnit_Extensions_MockFunction( $this->test_function_name, $this->test_scope_object ); 85 | 86 | // Return normally, only checks the call. 87 | $this->object->expects( $this->any() )->will( $this->returnValue( 'OK' ) ); 88 | 89 | $this->assertSame( 'OK', $this->test_scope_object->callFunction( $this->test_function_name, array() ) ); 90 | 91 | $this->object->restore(); 92 | } 93 | 94 | /** 95 | * When runkit.internal_override is Off, we cannot mock internal functions, 96 | * so we make a proxy around them and mock the proxy. 97 | * 98 | * @staticvar boolean $internal_override_on Tell is the config value is on. 99 | * @param type $function_name The (internal) function name to mock. 100 | * @return string The final function name (either proxy or the original). 101 | */ 102 | protected static function getFunctionName( $function_name ) 103 | { 104 | // Memoization for config value. 105 | static $internal_override_on; 106 | 107 | if ( !isset( $internal_override_on ) ) 108 | { 109 | $internal_override_on = (bool) ini_get( 'runkit.internal_override' ); 110 | } 111 | 112 | if ( $internal_override_on ) 113 | { 114 | return $function_name; 115 | } 116 | 117 | $proxy_function_name = 'proxy_to_' . $function_name . '_' . uniqid(); 118 | 119 | eval( << 157 | -------------------------------------------------------------------------------- /Tests/Extensions/MockStaticMethodTest.php: -------------------------------------------------------------------------------- 1 | test_scope_object = new TestScopeObjectForStatic(); 38 | } 39 | 40 | /** 41 | * Test simple function return faking without consraints. 42 | */ 43 | public function testMockWithReturn() 44 | { 45 | $this->object = new PHPUnit_Extensions_MockStaticMethod( 'TestStatic::test', $this->test_scope_object ); 46 | 47 | // CHanging return value. 48 | $this->object->expects( $this->atLeastOnce() )->will( $this->returnValue( 'DEF' ) ); 49 | 50 | $this->assertSame( 'DEF', $this->test_scope_object->callStatic() ); 51 | 52 | // From this scope the original method is called. 53 | $this->assertSame( 'ABC', TestStatic::test() ); 54 | 55 | $this->object->restore(); 56 | 57 | // We are back in 1985. 58 | $this->assertSame( 'ABC', $this->test_scope_object->callStatic() ); 59 | } 60 | 61 | } 62 | 63 | /** 64 | * Class to be used as scope object for mocked function calls. 65 | */ 66 | class TestScopeObjectForStatic 67 | { 68 | /** 69 | * Calls the TestStatic object. 70 | * 71 | * @return mixed The result of the static call. 72 | */ 73 | public function callStatic() 74 | { 75 | return TestStatic::test(); 76 | } 77 | } 78 | 79 | /** 80 | * Static object to test method overriding. 81 | */ 82 | class TestStatic 83 | { 84 | /** 85 | * Method to override. 86 | * 87 | * @return string 88 | */ 89 | public static function test() 90 | { 91 | return 'ABC'; 92 | } 93 | } 94 | 95 | ?> 96 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tcz/phpunit-mockfunction", 3 | "type": "library", 4 | "description": "PHPUnit extension that uses runkit to mock PHP functions (both user-defined and system)", 5 | "keywords": ["phpunit", "unit", "testing", "runkit", "mock"], 6 | "require": { 7 | "ext-runkit": "*" 8 | }, 9 | "autoload": { 10 | "classmap": [ 11 | "PHPUnit/" 12 | ] 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ./Tests 4 | 5 | 6 | 7 | 8 | PHPUnit 9 | 10 | 11 | 12 | 13 | 14 | ./PHPUnit 15 | 16 | 17 | 18 | --------------------------------------------------------------------------------