├── .github └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── RoboFile.php ├── VERSION ├── codeception.yml ├── composer.json ├── docs ├── ClassProxy.md ├── FuncProxy.md ├── InstanceProxy.md └── Test.md ├── src └── AspectMock │ ├── Core │ ├── Mocker.php │ └── Registry.php │ ├── Intercept │ ├── BeforeMockTransformer.php │ ├── FunctionInjector.php │ ├── MethodInvocation.php │ └── before_mock.php │ ├── Kernel.php │ ├── Proxy │ ├── Anything.php │ ├── AnythingClassProxy.php │ ├── ClassProxy.php │ ├── FuncProxy.php │ ├── FuncVerifier.php │ ├── InstanceProxy.php │ └── Verifier.php │ ├── Test.php │ └── Util │ ├── ArgumentsFormatter.php │ └── Undefined.php └── tests ├── _bootstrap.php ├── _data ├── autoload.php ├── demo.php ├── demo │ ├── AdminUserModel.php │ ├── MegaClass.php │ ├── TraitedClass1.php │ ├── TraitedClass2.php │ ├── TraitedClassTrait.php │ ├── UserModel.php │ ├── UserService.php │ └── WorkingTrait.php └── php7.php ├── _helpers ├── CodeGuy.php └── CodeHelper.php ├── _log └── .gitignore ├── unit.suite.yml └── unit ├── AccessDemoClassesTest.php ├── ClassProxyTest.php ├── FunctionInjectorTest.php ├── MockFailedTest.php ├── MockTest.php ├── StubTest.php ├── VerifierTest.php └── testDoubleTest.php /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: ~ 5 | pull_request: ~ 6 | workflow_dispatch: ~ 7 | schedule: 8 | - cron: '37 6 * * 5' 9 | 10 | jobs: 11 | tests: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | php: [7.4] 17 | 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v2 21 | 22 | - name: Setup PHP 23 | uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: ${{ matrix.php }} 26 | 27 | - name: Validate composer.json 28 | run: composer validate 29 | 30 | - name: Install dependencies 31 | run: composer update -n --prefer-source 32 | 33 | - name: Run test suite 34 | run: php vendor/bin/codecept run 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .idea 3 | composer.phar 4 | tests/_data/cache 5 | composer.lock 6 | tests/_helpers/_generated -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | #### 3.0.0 4 | 5 | * Upgraded to Go AOP 2.2 6 | * PHPUnit > 6.x support 7 | 8 | #### 2.1.0 9 | 10 | * Add missing type hints and parameter types to FuncProxy. See #135 by @SenseException 11 | * Added support for optional and referenced parameters in built-in functions. See #133 by @bhoehl 12 | * Fixed `return void` issue #123 by @zuozp8. See #130 13 | 14 | #### 2.0.0 15 | 16 | * **Updated to Go AOP Framework 2.0** 17 | * PHP7 (with typehints) supported 18 | * Minimal PHP version is PHP 5.6 19 | 20 | #### 1.0.0 21 | 22 | * **Updated to Go AOP Framework 1.0** 23 | * Added verifying inherited method calls by @torreytsui. See #90 24 | * Replaces `return` with `yield` for non-root namespaces. By @faridanthony See #93 25 | * Fix bug that class does not bind in static double by @torreytsui. See #89 26 | 27 | #### 0.5.5 28 | 29 | * compatible with Symfony3 and PHP7 30 | 31 | #### 0.5.4 32 | 33 | * Improved namespace handling 34 | * Added ability to display actually passed parameter in the error message 35 | * Fixed counting of dynamic class methods (#24) 36 | * Fixes for functions that have a brace as default on parametersi 37 | * Replace return with yield when docComments returns Generator 38 | 39 | 40 | #### 0.5.3 41 | 42 | * Updated to goaop/framework 0.6.x and codeception 2.1 43 | 44 | 45 | #### 0.5.1 46 | 47 | * Fixed strict errors for func verifier 48 | 49 | 50 | #### 0.5.0 51 | 52 | * `test::ns` method removed 53 | * Mocking functions implemented with `test::func` method 54 | * Fixed mocking functions with arguments passed by reference (#34) 55 | * Fixed passing arguments by reference in InstanceProxy 56 | * Debug mode can be disabled in options with `debug => false` 57 | * Updated to Go\Aop 0.5.0 58 | 59 | 60 | #### 0.5.0-beta 05/14/2014 61 | 62 | * Moved to Go\Aop 0.5.x-dev 63 | 64 | 65 | #### 0.4.2 05/09/2014 66 | 67 | * Depdendency on AspectMock ~0.4.0 68 | 69 | 70 | #### 0.4.1 71 | 72 | * RoboFile 73 | * Verify invocation arguments with closures 74 | * Verify invocation arguments with closures 75 | * Vetter support for traits 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Codeception PHP Testing Framework 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AspectMock 2 | ========== 3 | 4 | AspectMock is not an ordinary PHP mocking framework. 5 | With the power of Aspect Oriented programming and the awesome [Go-AOP](https://github.com/goaop/framework) library, 6 | AspectMock allows you to stub and mock practically anything in your PHP code! 7 | 8 | **Documentation** | [Test Doubles Builder](https://github.com/Codeception/AspectMock/blob/master/docs/Test.md) | [ClassProxy](https://github.com/Codeception/AspectMock/blob/master/docs/ClassProxy.md) | [InstanceProxy](https://github.com/Codeception/AspectMock/blob/master/docs/InstanceProxy.md) | [FuncProxy](https://github.com/Codeception/AspectMock/blob/master/docs/FuncProxy.md) 9 | 10 | [![Actions Status](https://github.com/Codeception/AspectMock/workflows/CI/badge.svg)](https://github.com/Codeception/AspectMock/actions) 11 | [![Latest Stable Version](https://poser.pugx.org/codeception/aspect-mock/v/stable.png)](https://packagist.org/packages/codeception/aspect-mock) 12 | [![Total Downloads](https://poser.pugx.org/codeception/aspect-mock/downloads)](https://packagist.org/packages/codeception/aspect-mock) 13 | [![Monthly Downloads](https://poser.pugx.org/codeception/aspect-mock/d/monthly)](https://packagist.org/packages/codeception/aspect-mock) 14 | [![StandWithUkraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md) 15 | 16 | ## Motivation 17 | 18 | PHP is a language that was not designed to be testable. Really. 19 | How would you fake the `time()` function to produce the same result for each test call? 20 | Is there any way to stub a static method of a class? Can you redefine a class method at runtime? 21 | Dynamic languages like Ruby or JavaScript allow us to do this. 22 | These features are essential for testing. AspectMock to the rescue! 23 | 24 | Thousands of lines of untested code are written everyday in PHP. 25 | In most cases, this code is not actually bad, 26 | but PHP does not provide capabilities to test it. You may suggest rewriting it from scratch following test driven design practices and use dependency injection wherever possible. Should this be done for stable working code? Well, there are much better ways to waste time. 27 | 28 | With AspectMock you can unit-test practically any OOP code. PHP powered with AOP incorporates features of dynamic languages we have long been missing. There is no excuse for not testing your code. 29 | You do not have to rewrite it from scratch to make it testable. Just install AspectMock with PHPUnit or Codeception and try to write some tests. It's really, really simple! 30 | 31 | 32 | ## Features 33 | 34 | * Create test doubles for **static methods**. 35 | * Create test doubles for **class methods called anywhere**. 36 | * Redefine methods on the fly. 37 | * Simple syntax that's easy to remember. 38 | 39 | ## Code Pitch 40 | 41 | #### Allows stubbing and mocking of static methods. 42 | 43 | Let's redefine static methods and verify their calls at runtime. 44 | 45 | ``` php 46 | assertSame('users', UserModel::tableName()); 51 | $userModel = test::double('UserModel', ['tableName' => 'my_users']); 52 | $this->assertSame('my_users', UserModel::tableName()); 53 | $userModel->verifyInvoked('tableName'); 54 | } 55 | ``` 56 | 57 | #### Allows replacement of class methods. 58 | 59 | Testing code developed with the **ActiveRecord** pattern. Does the use of the ActiveRecord pattern sound like bad practice? No. But the code below is untestable in classic unit testing. 60 | 61 | ``` php 62 | setName($name); 68 | $user->save(); 69 | } 70 | } 71 | ``` 72 | 73 | Without AspectMock you need to introduce `User` as an explicit dependency into class `UserService` to get it tested. 74 | But lets leave the code as it is. It works. Nevertheless, we should still test it to avoid regressions. 75 | 76 | We don't want the `$user->save` method to actually get executed, as it will hit the database. 77 | Instead we will replace it with a dummy and verify that it gets called by `createUserByName`: 78 | 79 | ``` php 80 | null]); 85 | $service = new UserService; 86 | $service->createUserByName('davert'); 87 | $this->assertSame('davert', $user->getName()); 88 | $user->verifyInvoked('save'); 89 | } 90 | ``` 91 | 92 | #### Intercept even parent class methods and magic methods 93 | 94 | ``` php 95 | null])); 101 | test::double('User', ['findByNameAndEmail' => new User(['name' => 'jon'])])); 102 | $user = User::findByNameAndEmail('jon','jon@coltrane.com'); // magic method 103 | $this->assertSame('jon', $user->getName()); 104 | $user->save(['name' => 'miles']); // ActiveRecord->save did not hit database 105 | $AR->verifyInvoked('save'); 106 | $this->assertSame('miles', $user->getName()); 107 | } 108 | ``` 109 | 110 | #### Override even standard PHP functions 111 | 112 | ``` php 113 | assertSame('now', time()); 118 | ``` 119 | 120 | #### Beautifully simple 121 | 122 | Only 4 methods are necessary for method call verification and one method to define test doubles: 123 | 124 | ``` php 125 | 'davert']); 130 | $this->assertSame('davert', $user->getName()); 131 | $user->verifyInvoked('getName'); 132 | $user->verifyInvokedOnce('getName'); 133 | $user->verifyNeverInvoked('setName'); 134 | $user->verifyInvokedMultipleTimes('setName',1); 135 | } 136 | ``` 137 | 138 | To check that method `setName` was called with `davert` as argument. 139 | 140 | ``` php 141 | verifyMethodInvoked('setName', ['davert']); 143 | ``` 144 | 145 | ## Wow! But how does it work? 146 | 147 | No PECL extensions is required. The [Go! AOP](http://go.aopphp.com/) library does the heavy lifting by patching autoloaded PHP classes on the fly. By introducing pointcuts to every method call, Go! allows intercepting practically any call to a method. AspectMock is a very tiny framework consisting of only 8 files using the power of the [Go! AOP Framework](http://go.aopphp.com/). Check out Aspect Oriented Development and the Go! library itself. 148 | 149 | ## Requirements 150 | 151 | * `PHP 8.2+` 152 | * `Go! AOP 4.x` 153 | 154 | ## Installation 155 | 156 | ### 1. Add aspect-mock to your composer.json. 157 | 158 | (lines with goaop @dev are needed depending on the `minimum-stability` in your composer settings) 159 | 160 | ``` 161 | { 162 | "require-dev": { 163 | "codeception/aspect-mock": "*", 164 | "goaop/framework": "@dev", 165 | "goaop/parser-reflection": "@dev" 166 | } 167 | } 168 | ``` 169 | 170 | ### 2. Install AspectMock with Go! AOP as a dependency. 171 | 172 | ``` 173 | php composer.phar update 174 | ``` 175 | 176 | ## Configuration 177 | 178 | Include `AspectMock\Kernel` class into your tests bootstrap file. 179 | 180 | ### With Composer's Autoloader 181 | 182 | ``` php 183 | init([ 189 | 'debug' => true, 190 | 'includePaths' => [__DIR__.'/../src'] 191 | ]); 192 | ``` 193 | 194 | If your project uses Composer's autoloader, that's all you need to get started. 195 | 196 | ### With Custom Autoloader 197 | 198 | If you use a custom autoloader (like in Yii/Yii2 frameworks), you should explicitly point AspectMock to modify it: 199 | 200 | ``` php 201 | init([ 207 | 'debug' => true, 208 | 'includePaths' => [__DIR__.'/../src'] 209 | ]); 210 | $kernel->loadFile('YourAutoloader.php'); // path to your autoloader 211 | ``` 212 | 213 | Load all autoloaders of your project this way, if you do not rely on Composer entirely. 214 | 215 | ### Without Autoloader 216 | 217 | If it still doesn't work for you... 218 | 219 | Explicitly load all required files before testing: 220 | 221 | 222 | ``` php 223 | init([ 229 | 'debug' => true, 230 | 'includePaths' => [__DIR__.'/../src'] 231 | ]); 232 | require 'YourAutoloader.php'; 233 | $kernel->loadPhpFiles('/../common'); 234 | ``` 235 | 236 | ### Customization 237 | 238 | There are a few options you can customize setting up AspectMock. All them are defined in Go! Framework. 239 | They might help If you still didn't get AspectMock running on your project. 240 | 241 | * `appDir` defines the root of web application which is being tested. All classes outside the root will be replaced with the proxies generated by AspectMock. By default it is a directory in which `vendor` dir of composer if located. **If you don't use Composer** or you have custom path to composer's vendor's folder, you should specify appDir 242 | * `cacheDir` a dir where updated source PHP files can be stored. If this directory is not set, proxie classes will be built on each run. Otherwise all PHP files used in tests will be updated with aspect injections and stored into `cacheDir` path. 243 | * `includePaths` directories with files that should be enhanced by Go Aop. Should point to your applications source files as well as framework files and any libraries you use.. 244 | * `excludePaths` a paths in which PHP files should not be affected by aspects. **You should exclude your tests files from interception**. 245 | 246 | Example: 247 | 248 | 249 | ``` php 250 | init([ 254 | 'appDir' => __DIR__ . '/../../', 255 | 'cacheDir' => '/tmp/myapp', 256 | 'includePaths' => [__DIR__.'/../src'] 257 | 'excludePaths' => [__DIR__] // tests dir should be excluded 258 | ]); 259 | ``` 260 | 261 | [More configs for different frameworks](https://github.com/Codeception/AspectMock/wiki/Example-configs). 262 | 263 | **It's pretty important to configure AspectMock properly. Otherwise it may not work as expected or you get side effects. Please make sure you included all files that you need to mock, but your test files as well as testing frameworks are excluded.** 264 | 265 | 266 | ## Usage in PHPUnit 267 | 268 | Use newly created `bootstrap` in your `phpunit.xml` configuration. Also disable `backupGlobals`: 269 | 270 | ``` xml 271 | 272 | ``` 273 | 274 | Clear the test doubles registry between tests. 275 | 276 | ``` php 277 | null]); 291 | \demo\UserModel::tableName(); 292 | \demo\UserModel::tableName(); 293 | $user->verifyInvokedMultipleTimes('tableName',2); 294 | } 295 | ``` 296 | 297 | ## Usage in Codeception. 298 | 299 | Include `AspectMock\Kernel` into `tests/_bootstrap.php`. 300 | We recommend including a call to `test::clean()` from your `CodeHelper` class: 301 | 302 | ``` php 303 | 'AspectMock\Test', 9 | 'docs/ClassProxy.md' => 'AspectMock\Proxy\ClassProxy', 10 | 'docs/InstanceProxy.md' => 'AspectMock\Proxy\InstanceProxy', 11 | 'docs/FuncProxy.md' => 'AspectMock\Proxy\FuncProxy' 12 | ]; 13 | 14 | protected function version() 15 | { 16 | return file_get_contents(__DIR__.'/VERSION'); 17 | } 18 | 19 | public function release() 20 | { 21 | $this->say("Releasing AspectMock"); 22 | 23 | $this->test(); 24 | 25 | $this->docs(); 26 | 27 | $this->taskGitStack() 28 | ->add('CHANGELOG.md') 29 | ->commit('updated') 30 | ->push() 31 | ->run(); 32 | 33 | $this->taskGitStack() 34 | ->tag($this->version()) 35 | ->push(' --tags') 36 | ->run(); 37 | 38 | $this->bump(); 39 | } 40 | 41 | public function docs() 42 | { 43 | foreach ($this->docs as $file => $class) { 44 | class_exists($class, true); 45 | $this->taskGenDoc($file) 46 | ->docClass($class) 47 | ->filterMethods(function(\ReflectionMethod $method) { 48 | if ($method->isConstructor() or $method->isDestructor()) return false; 49 | if (!$method->isPublic()) return false; 50 | if (strpos($method->name, '_') === 0) return false; 51 | return true; 52 | }) 53 | ->processMethodDocBlock( 54 | function (\ReflectionMethod $m, $doc) { 55 | $doc = str_replace(array('@since'), array(' * available since version'), $doc); 56 | $doc = str_replace(array(' @', "\n@"), array(" * ", "\n * "), $doc); 57 | return $doc; 58 | }) 59 | ->processProperty(false) 60 | ->run(); 61 | } 62 | } 63 | 64 | public function changed($addition) 65 | { 66 | $this->taskChangelog() 67 | ->version($this->version()) 68 | ->change($addition) 69 | ->run(); 70 | } 71 | 72 | public function bump($version = null) 73 | { 74 | if (!$version) { 75 | $versionParts = explode('.', $this->version()); 76 | $versionParts[count($versionParts)-1]++; 77 | $version = implode('.', $versionParts); 78 | } 79 | 80 | file_put_contents('VERSION', $version); 81 | } 82 | 83 | public function test() 84 | { 85 | $res = $this->taskCodecept()->run(); 86 | if (!$res) { 87 | $this->say('Tests did not pass, release declined'); 88 | exit; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 3.0.0 -------------------------------------------------------------------------------- /codeception.yml: -------------------------------------------------------------------------------- 1 | paths: 2 | tests: tests 3 | log: tests/_log 4 | data: tests/_data 5 | helpers: tests/_helpers 6 | bootstrap: _bootstrap.php 7 | settings: 8 | suite_class: \PHPUnit_Framework_TestSuite 9 | colors: true 10 | memory_limit: 1024M 11 | log: true 12 | coverage: 13 | enabled: true 14 | include: 15 | - src/* -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codeception/aspect-mock", 3 | "description": "Experimental Mocking Framework powered by Aspects", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Michael Bodnarchuk", 8 | "email": "davert@codeception.com" 9 | } 10 | ], 11 | "autoload": { 12 | "psr-0": { 13 | "AspectMock": "src/" 14 | } 15 | }, 16 | "require": { 17 | "php": "^8.2", 18 | "goaop/framework": "^4.0@dev", 19 | "goaop/parser-reflection": "^4.0@dev", 20 | "phpunit/phpunit": "^9.5", 21 | "symfony/finder": "^4.4 | ^5.4 | ^6.0" 22 | }, 23 | "require-dev": { 24 | "codeception/codeception": "^4.1", 25 | "codeception/verify": "^2.2", 26 | "codeception/specify": "^2.0", 27 | "consolidation/robo": "^3.0" 28 | }, 29 | "extra": { 30 | "branch-alias": { 31 | "dev-master": "5.0-dev" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/ClassProxy.md: -------------------------------------------------------------------------------- 1 | 2 | ## AspectMock\Proxy\ClassProxy 3 | 4 | * *Extends* `AspectMock\Proxy\Verifier` 5 | 6 | ClassProxy represents a class of your project. 7 | 8 | * It can be used to verify methods invocations of a class. 9 | * It provides some nice functions to construct class instances, with or without a constructor. 10 | * It can be used to check class definitions. 11 | 12 | 13 | ```php 14 | construct(); 18 | $user->save(); 19 | $userModel->verifyInvoked('tableName'); 20 | $userModel->verifyInvoked('save'); 21 | ``` 22 | 23 | You can get a class name of a proxy via `className` property. 24 | 25 | ```php 26 | className; // UserModel 29 | ``` 30 | 31 | Also, you can get the list of calls for a specific method. 32 | 33 | ```php 34 | someMethod('arg1', 'arg2'); 37 | $user->getCallsForMethod('someMethod') // [ ['arg1', 'arg2'] ] 38 | ``` 39 | 40 | #### *public* getCallsForMethod($method) 41 | #### *public* isDefined() 42 | Returns true if class exists. 43 | Returns false if class is not defined yet, and was declared via `test::spec`. 44 | 45 | #### *public* interfaces() 46 | Returns an array with all interface names of a class 47 | 48 | #### *public* parent() 49 | Returns a name of the parent of a class. 50 | 51 | * return null 52 | 53 | #### *public* hasMethod($method) 54 | * `param mixed` $method 55 | 56 | #### *public* hasProperty($property) 57 | * `param mixed` $property 58 | 59 | #### *public* traits() 60 | Returns array of all trait names of a class. 61 | 62 | #### *public* construct() 63 | Creates an instance of a class via constructor. 64 | 65 | ```php 66 | construct([ 68 | 'name' => 'davert', 69 | 'email' => 'davert@mail.ua' 70 | ]); 71 | ``` 72 | 73 | #### *public* make() 74 | Creates a class instance without calling a constructor. 75 | 76 | ```php 77 | make(); 79 | ``` 80 | 81 | #### *public* verifyInvoked($name, array $params = null) 82 | Verifies a method was invoked at least once. 83 | In second argument you can specify with which params method expected to be invoked; 84 | 85 | ``` php 86 | verifyInvoked('save'); 88 | $user->verifyInvoked('setName',['davert']); 89 | 90 | ``` 91 | 92 | #### *public* verifyInvokedOnce($name, array $params = null) 93 | Verifies that method was invoked only once. 94 | 95 | #### *public* verifyInvokedMultipleTimes($name, $times, array $params = null) 96 | Verifies that method was called exactly $times times. 97 | 98 | ``` php 99 | verifyInvokedMultipleTimes('save',2); 101 | $user->verifyInvokedMultipleTimes('dispatchEvent',3,['before_validate']); 102 | $user->verifyInvokedMultipleTimes('dispatchEvent',4,['after_save']); 103 | ``` 104 | 105 | * throws ExpectationFailedException 106 | 107 | #### *public* verifyNeverInvoked($name, array $params = null) 108 | Verifies that method was not called. 109 | In second argument with which arguments is not expected to be called. 110 | 111 | ``` php 112 | setName('davert'); 114 | $user->verifyNeverInvoked('setName'); // fail 115 | $user->verifyNeverInvoked('setName',['davert']); // fail 116 | $user->verifyNeverInvoked('setName',['bob']); // success 117 | $user->verifyNeverInvoked('setName',[]); // success 118 | ``` 119 | 120 | * throws ExpectationFailedException 121 | 122 | 123 | -------------------------------------------------------------------------------- /docs/FuncProxy.md: -------------------------------------------------------------------------------- 1 | 2 | ## AspectMock\Proxy\FuncProxy 3 | 4 | 5 | 6 | FuncProxy is a wrapper around mocked function, used to verify function calls. 7 | Has the same verification methods as `InstanceProxy` and `ClassProxy` do. 8 | 9 | Usage: 10 | 11 | ```php 12 | verifyInvoked(); // true 18 | $func->verifyInvoked(['hello']); // true 19 | $func->verifyInvokedMultipleTimes(2); 20 | $func->verifyNeverInvoked(['bye']); 21 | ``` 22 | 23 | #### *public* verifyInvoked(array $params = null) 24 | #### *public* verifyInvokedOnce(array $params = null) 25 | #### *public* verifyNeverInvoked(array $params = null) 26 | #### *public* verifyInvokedMultipleTimes($times, array $params = null) 27 | #### *public* getCallsForMethod($func) 28 | 29 | -------------------------------------------------------------------------------- /docs/InstanceProxy.md: -------------------------------------------------------------------------------- 1 | 2 | ## AspectMock\Proxy\InstanceProxy 3 | 4 | * *Extends* `AspectMock\Proxy\Verifier` 5 | 6 | InstanceProxy is a proxy for underlying object, mocked with test::double. 7 | A real object can be returned with `getObject` methods. 8 | 9 | ``` php 10 | getObject(); // true 17 | 18 | ``` 19 | 20 | Contains verification methods and `class` property that points to `ClassProxy`. 21 | 22 | ``` php 23 | 'davert']); 25 | $user = test::double(new User); 26 | // now $user is a proxy class of user 27 | $this->assertSame('davert', $user->getName()); // success 28 | $user->verifyInvoked('getName'); // success 29 | $this->assertInstanceOf('User', $user); // fail 30 | ``` 31 | 32 | A `class` property allows to verify method calls to any instance of this class. 33 | Constains a **ClassVerifier** object. 34 | 35 | ``` php 36 | class->hasMethod('save'); 39 | $user->setName('davert'); 40 | $user->class->verifyInvoked('setName'); 41 | ``` 42 | Also, you can get the list of calls for a specific method. 43 | 44 | ```php 45 | someMethod('arg1', 'arg2'); 48 | $user->getCallsForMethod('someMethod') // [ ['arg1', 'arg2'] ] 49 | ``` 50 | 51 | #### *public* getObject() 52 | Returns a real object that is proxified. 53 | 54 | * return mixed 55 | 56 | #### *public* getCallsForMethod($method) 57 | #### *public* verifyInvoked($name, array $params = null) 58 | Verifies a method was invoked at least once. 59 | In second argument you can specify with which params method expected to be invoked; 60 | 61 | ``` php 62 | verifyInvoked('save'); 64 | $user->verifyInvoked('setName',['davert']); 65 | 66 | ``` 67 | 68 | #### *public* verifyInvokedOnce($name, array $params = null) 69 | Verifies that method was invoked only once. 70 | 71 | #### *public* verifyInvokedMultipleTimes($name, $times, array $params = null) 72 | Verifies that method was called exactly $times times. 73 | 74 | ``` php 75 | verifyInvokedMultipleTimes('save',2); 77 | $user->verifyInvokedMultipleTimes('dispatchEvent',3,['before_validate']); 78 | $user->verifyInvokedMultipleTimes('dispatchEvent',4,['after_save']); 79 | ``` 80 | 81 | * throws ExpectationFailedException 82 | 83 | #### *public* verifyNeverInvoked($name, array $params = null) 84 | Verifies that method was not called. 85 | In second argument with which arguments is not expected to be called. 86 | 87 | ``` php 88 | setName('davert'); 90 | $user->verifyNeverInvoked('setName'); // fail 91 | $user->verifyNeverInvoked('setName',['davert']); // fail 92 | $user->verifyNeverInvoked('setName',['bob']); // success 93 | $user->verifyNeverInvoked('setName',[]); // success 94 | ``` 95 | 96 | * throws ExpectationFailedException 97 | 98 | 99 | -------------------------------------------------------------------------------- /docs/Test.md: -------------------------------------------------------------------------------- 1 | 2 | ## AspectMock\Test 3 | 4 | 5 | 6 | `AspectMock\Test` class is a builder of test doubles. 7 | Any object can be enhanced and turned to a test double with the call to `double` method. 8 | Mocking abstract classes and interfaces is not supported at this time. 9 | This allows to redefine any method of object with your own, and adds mock verification methods. 10 | 11 | **Recommended Usage**: 12 | 13 | ``` php 14 | 'davert']); 33 | $user->getName() // => davert 34 | $user->verifyInvoked('getName'); // => success 35 | $user->getObject() // => returns instance of User, i.e. real, not proxified object 36 | 37 | # with closure 38 | $user = test::double(new User, ['getName' => function() { return $this->login; }]); 39 | $user->login = 'davert'; 40 | $user->getName(); // => davert 41 | 42 | # on a class 43 | $ar = test::double('ActiveRecord', ['save' => null]); 44 | $user = new User; 45 | $user->name = 'davert'; 46 | $user->save(); // passes to ActiveRecord->save() and does not insert any SQL. 47 | $ar->verifyInvoked('save'); // true 48 | 49 | # on static method call 50 | User::tableName(); // 'users' 51 | $user = test::double('User', ['tableName' => 'fake_users']); 52 | User::tableName(); // 'fake_users' 53 | $user->verifyInvoked('tableName'); // success 54 | 55 | # append declaration 56 | $user = new User; 57 | test::double($user, ['getName' => 'davert']); 58 | test::double($user, ['getEmail' => 'davert@mail.ua']); 59 | $user->getName(); // => 'davert' 60 | $user->getEmail(); => 'davert@mail.ua' 61 | 62 | # create an instance of mocked class 63 | test::double('User')->construct(['name' => 'davert']); // via constructor 64 | test::double('User')->make(); // without calling constructor 65 | 66 | # stub for magic method 67 | test::double('User', ['findByUsernameAndPasswordAndEmail' => false]); 68 | User::findByUsernameAndPasswordAndEmail(); // null 69 | 70 | # stub for method of parent class 71 | # if User extends ActiveRecord 72 | test::double('ActiveRecord', ['save' => false]); 73 | $user = new User(['name' => 'davert']); 74 | $user->save(); // false 75 | 76 | ``` 77 | 78 | * api 79 | * `param string|object` $classOrObject 80 | * `param array` $params [ 'methodName' => 'returnValue' ] 81 | * throws Exception 82 | * return Verifier|Proxy\ClassProxy|Proxy\InstanceProxy 83 | 84 | #### *public static* spec($classOrObject, array $params = Array ( ) ) 85 | If you follow TDD/BDD practices a test should be written before the class is defined. 86 | If you would call undefined class in a test, a fatal error will be triggered. 87 | Instead you can use `test::spec` method that will create a proxy for an undefined class. 88 | 89 | ``` php 90 | defined(); // false 93 | ``` 94 | 95 | You can create instances of undefined classes and play with them: 96 | 97 | ``` php 98 | construct(); 100 | $user->setName('davert'); 101 | $user->setNumPosts(count($user->getPosts())); 102 | $this->assertSame('davert', $user->getName()); // fail 103 | ``` 104 | 105 | The test will be executed normally and will fail at the first assertion. 106 | 107 | `test::spec()->construct` creates an instance of `AspectMock\Proxy\Anything` 108 | which tries not to cause errors whatever you try to do with it. 109 | 110 | ``` php 111 | construct(); 113 | $user->can()->be->called()->anything(); 114 | $user->can['be used as array']; 115 | foreach ($user->names as $name) { 116 | $name->canBeIterated(); 117 | } 118 | ``` 119 | 120 | None of those calls will trigger an error in your test. 121 | Thus, you can write a valid test before the class is declared. 122 | 123 | If class is already defined, `test::spec` will act as `test::double`. 124 | 125 | * api 126 | * `param string|object` $classOrObject 127 | * `param array` $params 128 | * return Verifier|Proxy\ClassProxy|Proxy\InstanceProxy 129 | 130 | #### *public static* methods($classOrObject, array $only = Array ( ) ) 131 | Replaces all methods in a class with dummies, except those specified in the `$only` param. 132 | 133 | ``` php 134 | 'jon']); 136 | test::methods($user, ['getName']); 137 | $user->setName('davert'); // not invoked 138 | $user->getName(); // jon 139 | ``` 140 | 141 | You can create a dummy without a constructor with all methods disabled: 142 | 143 | ``` php 144 | make(); 146 | test::methods($user, []); 147 | ``` 148 | 149 | * api 150 | * `param string|object` $classOrObject 151 | * `param string[]` $only 152 | * return Verifier|Proxy\ClassProxy|Proxy\InstanceProxy 153 | * throws Exception 154 | 155 | #### *public static* func($namespace, $functionName, $body) 156 | Replaces function in provided namespace with user-defined function or value that function returns. 157 | Function is restored to original on cleanup. 158 | 159 | ```php 160 | verifyInvoked(); 183 | $func->verifyInvokedOnce(['Y']); 184 | ``` 185 | 186 | * `param mixed` $body whatever a function might return or Callable substitute 187 | * return Proxy\FuncProxy 188 | 189 | #### *public static* clean($classOrInstance = null) 190 | Clears test doubles registry. 191 | Should be called between tests. 192 | 193 | ``` php 194 | methodMap)) { 31 | $invocation = new MethodInvocation(); 32 | $invocation->setThis($class); 33 | $invocation->setMethod($method); 34 | $invocation->setArguments($params); 35 | $invocation->isStatic($static); 36 | $invocation->setDeclaredClass($declaredClass); 37 | } 38 | 39 | // Record actual method called, not faked method. 40 | if (in_array($method, $this->dynamicMethods)) { 41 | $method = array_shift($params); 42 | $params = array_shift($params); 43 | } 44 | 45 | if (!$static) { 46 | if (isset($this->objectMap[spl_object_hash($class)])) { 47 | Registry::registerInstanceCall($class, $method, $params); 48 | } 49 | 50 | $class = get_class($class); 51 | } 52 | 53 | if (isset($this->classMap[$class])) { 54 | Registry::registerClassCall($class, $method, $params); 55 | } 56 | 57 | if ($class != $declaredClass && isset($this->classMap[$declaredClass])) { 58 | Registry::registerClassCall($declaredClass, $method, $params); 59 | } 60 | 61 | if ($invocation instanceof MethodInvocation) { 62 | $result = $this->invokeFakedMethods($invocation); 63 | } 64 | 65 | return $result; 66 | } 67 | 68 | public function fakeFunctionAndRegisterCalls($namespace, $function, $args) 69 | { 70 | $result = __AM_CONTINUE__; 71 | $fullFuncName = sprintf('%s\%s', $namespace, $function); 72 | Registry::registerFunctionCall($fullFuncName, $args); 73 | 74 | if (array_key_exists($fullFuncName, $this->funcMap)) { 75 | $func = $this->funcMap[$fullFuncName]; 76 | $result = is_callable($func) ? call_user_func_array($func, $args) : $func; 77 | } 78 | 79 | return $result; 80 | } 81 | 82 | /** 83 | * @return mixed 84 | */ 85 | protected function invokeFakedMethods(MethodInvocation $invocation) 86 | { 87 | $method = $invocation->getMethod(); 88 | if (!in_array($method, $this->methodMap)) { 89 | return __AM_CONTINUE__; 90 | } 91 | 92 | $obj = $invocation->getThis(); 93 | 94 | if (is_object($obj)) { 95 | // instance method 96 | $params = $this->getObjectMethodStubParams($obj, $method); 97 | if ($params !== false) { 98 | return $this->stub($invocation, $params); 99 | } 100 | 101 | // class method 102 | $params = $this->getClassMethodStubParams(get_class($obj), $method); 103 | if ($params !== false) { 104 | return $this->stub($invocation, $params); 105 | } 106 | 107 | // inheritance 108 | $params = $this->getClassMethodStubParams($invocation->getDeclaredClass(), $method); 109 | if ($params !== false) { 110 | return $this->stub($invocation, $params); 111 | } 112 | 113 | // magic methods 114 | if ($method == '__call') { 115 | $args = $invocation->getArguments(); 116 | $method = array_shift($args); 117 | 118 | $params = $this->getObjectMethodStubParams($obj, $method); 119 | if ($params !== false) { 120 | return $this->stubMagicMethod($invocation, $params); 121 | } 122 | 123 | // magic class method 124 | $params = $this->getClassMethodStubParams(get_class($obj), $method); 125 | if ($params !== false) { 126 | return $this->stubMagicMethod($invocation, $params); 127 | } 128 | 129 | // inheritance 130 | $calledClass = $invocation->getDeclaredClass(); 131 | $params = $this->getClassMethodStubParams($calledClass, $method); 132 | if ($params !== false) { 133 | return $this->stubMagicMethod($invocation, $params); 134 | } 135 | } 136 | } else { 137 | // static method 138 | $params = $this->getClassMethodStubParams($obj, $method); 139 | if ($params !== false) { 140 | return $this->stub($invocation, $params); 141 | } 142 | 143 | // inheritance 144 | $params = $this->getClassMethodStubParams($invocation->getDeclaredClass(), $method); 145 | if ($params !== false) { 146 | return $this->stub($invocation, $params); 147 | } 148 | 149 | // magic static method (facade) 150 | if ($method == '__callStatic') { 151 | $args = $invocation->getArguments(); 152 | $method = array_shift($args); 153 | 154 | $params = $this->getClassMethodStubParams($obj, $method); 155 | if ($params !== false) { 156 | return $this->stubMagicMethod($invocation, $params); 157 | } 158 | 159 | // inheritance 160 | $calledClass = $invocation->getDeclaredClass(); 161 | $params = $this->getClassMethodStubParams($calledClass, $method); 162 | if ($params !== false) { 163 | return $this->stubMagicMethod($invocation, $params); 164 | } 165 | } 166 | } 167 | 168 | return __AM_CONTINUE__; 169 | } 170 | 171 | protected function getObjectMethodStubParams($obj, $method_name) 172 | { 173 | $oid = spl_object_hash($obj); 174 | if (!isset($this->objectMap[$oid])) { 175 | return false; 176 | } 177 | 178 | $params = $this->objectMap[$oid]; 179 | if (!array_key_exists($method_name, $params)) { 180 | return false; 181 | } 182 | 183 | return $params; 184 | } 185 | 186 | protected function getClassMethodStubParams($class_name, $method_name) 187 | { 188 | if (!isset($this->classMap[$class_name])) { 189 | return false; 190 | } 191 | 192 | $params = $this->classMap[$class_name]; 193 | if (!array_key_exists($method_name, $params)) { 194 | return false; 195 | } 196 | 197 | return $params; 198 | } 199 | 200 | protected function stub(MethodInvocation $invocation, $params) 201 | { 202 | $name = $invocation->getMethod(); 203 | 204 | $replacedMethod = $params[$name]; 205 | 206 | $replacedMethod = $this->turnToClosure($replacedMethod); 207 | 208 | if ($invocation->isStatic()) { 209 | $replacedMethod = Closure::bind($replacedMethod, null, $invocation->getThis()); 210 | } else { 211 | $replacedMethod = $replacedMethod->bindTo($invocation->getThis(), get_class($invocation->getThis())); 212 | } 213 | 214 | return call_user_func_array($replacedMethod, $invocation->getArguments()); 215 | } 216 | 217 | protected function stubMagicMethod(MethodInvocation $invocation, array $params) 218 | { 219 | $args = $invocation->getArguments(); 220 | $name = array_shift($args); 221 | 222 | $replacedMethod = $params[$name]; 223 | $replacedMethod = $this->turnToClosure($replacedMethod); 224 | 225 | if ($invocation->isStatic()) { 226 | Closure::bind($replacedMethod, null, $invocation->getThis()); 227 | } else { 228 | $replacedMethod = $replacedMethod->bindTo($invocation->getThis(), get_class($invocation->getThis())); 229 | } 230 | 231 | return call_user_func_array($replacedMethod, $args); 232 | } 233 | 234 | 235 | protected function turnToClosure($returnValue): Closure 236 | { 237 | if ($returnValue instanceof Closure) { 238 | return $returnValue; 239 | } 240 | 241 | return fn() => $returnValue; 242 | } 243 | 244 | public function registerClass(string $class, array $params = []): void 245 | { 246 | $class = ltrim($class, '\\'); 247 | if (isset($this->classMap[$class])) { 248 | $params = array_merge($this->classMap[$class], $params); 249 | } 250 | 251 | $this->methodMap = array_merge($this->methodMap, array_keys($params)); 252 | $this->classMap[$class] = $params; 253 | } 254 | 255 | public function registerObject(object $object, array $params = []): void 256 | { 257 | $hash = spl_object_hash($object); 258 | if (isset($this->objectMap[$hash])) { 259 | $params = array_merge($this->objectMap[$hash], $params); 260 | } 261 | 262 | $this->objectMap[$hash] = $params; 263 | $this->methodMap = array_merge($this->methodMap, array_keys($params)); 264 | } 265 | 266 | /** 267 | * @param string|Closure $func 268 | */ 269 | public function registerFunc(string $namespace, $func, $body): void 270 | { 271 | $namespace = ltrim($namespace, '\\'); 272 | if (!function_exists("{$namespace}\\{$func}")) { 273 | $injector = new FunctionInjector($namespace, $func); 274 | $injector->save(); 275 | $injector->inject(); 276 | } 277 | 278 | $this->funcMap["{$namespace}\\{$func}"] = $body; 279 | } 280 | 281 | public function clean($objectOrClass = null): void 282 | { 283 | if (!$objectOrClass) { 284 | $this->classMap = []; 285 | $this->objectMap = []; 286 | $this->methodMap = ['__call', '__callStatic']; 287 | $this->funcMap = []; 288 | } elseif (is_object($objectOrClass)) { 289 | unset($this->objectMap[spl_object_hash($objectOrClass)]); 290 | } else { 291 | unset($this->classMap[$objectOrClass]); 292 | } 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /src/AspectMock/Core/Registry.php: -------------------------------------------------------------------------------- 1 | registerClass($name, $params); 29 | } 30 | 31 | public static function registerObject($object, $params = array()): void 32 | { 33 | self::$mocker->registerObject($object, $params); 34 | } 35 | 36 | public static function registerFunc($namespace, $function, $resultOrClosure): void 37 | { 38 | self::$mocker->registerFunc($namespace, $function, $resultOrClosure); 39 | } 40 | 41 | public static function getClassCallsFor($class) 42 | { 43 | $class = ltrim($class,'\\'); 44 | return self::$classCalls[$class] ?? []; 45 | } 46 | 47 | public static function getInstanceCallsFor($instance) 48 | { 49 | $oid = spl_object_hash($instance); 50 | return self::$instanceCalls[$oid] ?? []; 51 | } 52 | 53 | public static function getFuncCallsFor($func) 54 | { 55 | $func = ltrim($func,'\\'); 56 | return self::$funcCalls[$func] ?? []; 57 | } 58 | 59 | public static function clean($classOrInstance = null): void 60 | { 61 | $classOrInstance = self::getRealClassOrObject($classOrInstance); 62 | self::$mocker->clean($classOrInstance); 63 | if (is_object($classOrInstance)) { 64 | $oid = spl_object_hash($classOrInstance); 65 | unset(self::$instanceCalls[$oid]); 66 | 67 | } elseif (is_string($classOrInstance)) { 68 | unset(self::$classCalls[$classOrInstance]); 69 | 70 | } else { 71 | self::cleanInvocations(); 72 | } 73 | } 74 | 75 | public static function cleanInvocations(): void 76 | { 77 | self::$instanceCalls = []; 78 | self::$classCalls = []; 79 | self::$funcCalls = []; 80 | } 81 | 82 | public static function registerInstanceCall($instance, $method, $args = array()): void 83 | { 84 | $oid = spl_object_hash($instance); 85 | if (!isset(self::$instanceCalls[$oid])) self::$instanceCalls[$oid] = []; 86 | 87 | isset(self::$instanceCalls[$oid][$method]) 88 | ? self::$instanceCalls[$oid][$method][] = $args 89 | : self::$instanceCalls[$oid][$method] = array($args); 90 | 91 | } 92 | 93 | public static function registerClassCall($class, $method, $args = array()): void 94 | { 95 | if (!isset(self::$classCalls[$class])) self::$classCalls[$class] = []; 96 | 97 | isset(self::$classCalls[$class][$method]) 98 | ? self::$classCalls[$class][$method][] = $args 99 | : self::$classCalls[$class][$method] = array($args); 100 | 101 | } 102 | 103 | public static function registerFunctionCall($functionName, $args): void 104 | { 105 | if (!isset(self::$funcCalls[$functionName])) self::$funcCalls[$functionName] = []; 106 | 107 | isset(self::$funcCalls[$functionName]) 108 | ? self::$funcCalls[$functionName][] = $args 109 | : self::$funcCalls[$functionName] = array($args); 110 | } 111 | 112 | public static function getRealClassOrObject($classOrObject) 113 | { 114 | if ($classOrObject instanceof ClassProxy) return $classOrObject->className; 115 | 116 | if ($classOrObject instanceof InstanceProxy) return $classOrObject->getObject(); 117 | 118 | return $classOrObject; 119 | } 120 | 121 | public static function setMocker(Mocker $mocker): void 122 | { 123 | self::$mocker = $mocker; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/AspectMock/Intercept/BeforeMockTransformer.php: -------------------------------------------------------------------------------- 1 | uri, $metadata->syntaxTree); 22 | $namespaces = $reflectedFile->getFileNamespaces(); 23 | 24 | foreach ($namespaces as $namespace) { 25 | 26 | $classes = $namespace->getClasses(); 27 | foreach ($classes as $class) { 28 | 29 | // Skip interfaces 30 | if ($class->isInterface()) { 31 | continue; 32 | } 33 | 34 | // Look for aspects 35 | if (in_array(Aspect::class, $class->getInterfaceNames())) { 36 | continue; 37 | } 38 | 39 | /** @var ReflectionMethod[] $methods */ 40 | $methods = $class->getMethods(); 41 | foreach ($methods as $method) { 42 | if ($method->getDeclaringClass()->name != $class->getName()) { 43 | continue; 44 | } 45 | // methods from traits have the same declaring class name, so check that the filenames match, too 46 | if ($method->getFileName() != $class->getFileName()) { 47 | continue; 48 | } 49 | if ($method->isAbstract()) { 50 | continue; 51 | } 52 | $beforeDefinition = $method->isStatic() 53 | ? $this->beforeStatic 54 | : $this->before; 55 | 56 | // replace return with yield when method is Generator 57 | if ($method->isGenerator()) { 58 | $beforeDefinition = str_replace('return', 'yield', $beforeDefinition); 59 | } 60 | if (method_exists($method, 'getReturnType') && $method->getReturnType() == 'void') { 61 | //TODO remove method_exists($method, 'getReturnType') when support for php5 is dropped 62 | $beforeDefinition = str_replace('return $__am_res;', 'return;', $beforeDefinition); 63 | } 64 | 65 | $reflectedParams = $method->getParameters(); 66 | 67 | $params = []; 68 | 69 | foreach ($reflectedParams as $reflectedParam) { 70 | $params[] = ($reflectedParam->isPassedByReference() ? '&$' : '$') . $reflectedParam->getName(); 71 | } 72 | $params = implode(", ", $params); 73 | $beforeDefinition = sprintf($beforeDefinition, $params); 74 | $tokenPosition = $method->getNode()->getAttribute('startTokenPos'); 75 | do { 76 | if (($metadata->tokenStream[$tokenPosition]->text ?? '') === '{') { 77 | $metadata->tokenStream[$tokenPosition]->text .= $beforeDefinition; 78 | $result = self::RESULT_TRANSFORMED; 79 | break; 80 | } 81 | $tokenPosition++; 82 | } while (isset($metadata->tokenStream[$tokenPosition])); 83 | } 84 | } 85 | } 86 | 87 | return $result; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/AspectMock/Intercept/FunctionInjector.php: -------------------------------------------------------------------------------- 1 | namespace = $namespace; 50 | $this->function = $function; 51 | $this->placeOptionalAndReferenceFunction($namespace, $function); 52 | $this->place('ns', $this->namespace); 53 | $this->place('func', $this->function); 54 | } 55 | 56 | public function getParameterDeclaration(\ReflectionParameter $parameter, $internal) 57 | { 58 | $text = (string)$parameter; 59 | if (preg_match('#Parameter\s\#\d+\s\[\s<(required|optional)>(.*)(\sor NULL)(.*)\s]#', $text, $match)) { 60 | $text = $match[2].$match[4]; 61 | } elseif (preg_match('#Parameter\s\#\d+\s\[\s<(required|optional)>\s(.*)\s]#', $text, $match)) { 62 | $text = $match[2]; 63 | } else { 64 | throw new Exception('reflection api changed. adjust code.'); 65 | } 66 | 67 | if ($internal && $parameter->isOptional()) { 68 | $text .= "=NULL"; 69 | } 70 | 71 | return $text; 72 | } 73 | 74 | public function placeOptionalAndReferenceFunction($namespace, $function): void 75 | { 76 | $reflect = new ReflectionFunction($function); 77 | $parameters = []; 78 | $args = ''; 79 | $byRef = false; 80 | $optionals = false; 81 | $names = []; 82 | $internal = $reflect->isInternal(); 83 | foreach ($reflect->getParameters() as $parameter) { 84 | $name = '$'.$parameter->getName(); 85 | $newname = '$p'.$parameter->getPosition(); 86 | $declaration = str_replace($name, $newname, $this->getParameterDeclaration($parameter, $internal)); 87 | $name = $newname; 88 | if (!$optionals && $parameter->isOptional()) { 89 | $optionals = true; 90 | } 91 | 92 | if ($parameter->isPassedByReference()) { 93 | $name = '&'.$name; 94 | $byRef = true; 95 | } 96 | 97 | $names[] = $name; 98 | $parameters[$newname] = $declaration; 99 | } 100 | 101 | if ($byRef) { 102 | $this->template = $this->templateByRefOptional; 103 | $this->place('arguments', implode(', ', $parameters)); 104 | $code = ''; 105 | for ($i = count($parameters); $i > 0; --$i) { 106 | $code .= sprintf(' case %d: $args = [', $i) . implode(', ', $names) . "]; break;\n"; 107 | array_pop($names); 108 | } 109 | 110 | $this->place('code', $code); 111 | } 112 | } 113 | 114 | public function save(): void 115 | { 116 | $this->fileName = tempnam(sys_get_temp_dir(), $this->function); 117 | file_put_contents($this->fileName, $this->template); 118 | } 119 | 120 | public function inject(): void 121 | { 122 | require_once $this->fileName; 123 | } 124 | 125 | public function getFileName() 126 | { 127 | return $this->fileName; 128 | } 129 | 130 | public function getPHP() 131 | { 132 | return $this->template; 133 | } 134 | 135 | protected function place($var, $value): void 136 | { 137 | $this->template = str_replace(sprintf('{{%s}}', $var), $value, $this->template); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/AspectMock/Intercept/MethodInvocation.php: -------------------------------------------------------------------------------- 1 | declaredClass = $declaredClass; 27 | } 28 | 29 | /** 30 | * @return mixed 31 | */ 32 | public function getDeclaredClass() 33 | { 34 | return $this->declaredClass; 35 | } 36 | 37 | /** 38 | * @param mixed $isStatic 39 | */ 40 | public function isStatic($isStatic = null) 41 | { 42 | if ($isStatic === null) return $this->isStatic; 43 | 44 | $this->isStatic = $isStatic; 45 | } 46 | 47 | /** 48 | * @param mixed $class 49 | */ 50 | public function setThis($class): void 51 | { 52 | $this->class = $class; 53 | } 54 | 55 | /** 56 | * @return mixed 57 | */ 58 | public function getThis() 59 | { 60 | return $this->class; 61 | } 62 | 63 | /** 64 | * @param mixed $closure 65 | */ 66 | public function setClosure($closure): void 67 | { 68 | $this->closure = $closure; 69 | } 70 | 71 | /** 72 | * @return mixed 73 | */ 74 | public function getClosure() 75 | { 76 | return $this->closure; 77 | } 78 | 79 | /** 80 | * @param mixed $method 81 | */ 82 | public function setMethod($method): void 83 | { 84 | $this->method = $method; 85 | } 86 | 87 | /** 88 | * @return mixed 89 | */ 90 | public function getMethod() 91 | { 92 | return $this->method; 93 | } 94 | 95 | /** 96 | * @param mixed $params 97 | */ 98 | public function setArguments($params): void 99 | { 100 | $this->arguments = $params; 101 | } 102 | 103 | /** 104 | * @return mixed 105 | */ 106 | public function getArguments() 107 | { 108 | return $this->arguments; 109 | } 110 | 111 | } -------------------------------------------------------------------------------- /src/AspectMock/Intercept/before_mock.php: -------------------------------------------------------------------------------- 1 | fakeMethodsAndRegisterCalls($class, $declaredClass, $method, $params, $static); 9 | } 10 | 11 | function __amock_before_func($namespace, $func, $params) { 12 | return Registry::$mocker->fakeFunctionAndRegisterCalls($namespace, $func, $params); 13 | } 14 | 15 | const __AM_CONTINUE__ = '__am_continue__'; 16 | -------------------------------------------------------------------------------- /src/AspectMock/Kernel.php: -------------------------------------------------------------------------------- 1 | files()->name('*.php')->in($dir); 51 | foreach ($files as $file) { 52 | $this->loadFile($file->getRealpath()); 53 | } 54 | 55 | } 56 | 57 | /** 58 | * Includes file and injects aspect pointcuts into int 59 | */ 60 | public function loadFile(string $file) 61 | { 62 | include FilterInjectorTransformer::rewrite($file); 63 | } 64 | 65 | protected function registerTransformers(): array 66 | { 67 | $cachePathManager = $this->getContainer()->getService(CachePathManager::class); 68 | 69 | $sourceTransformers = [ 70 | new FilterInjectorTransformer($this, SourceTransformingLoader::getId(), $cachePathManager), 71 | new MagicConstantTransformer($this), 72 | new BeforeMockTransformer( 73 | $this, 74 | $this->getContainer()->getService(AdviceMatcher::class), 75 | $cachePathManager, 76 | $this->getContainer()->getService(CachedAspectLoader::class) 77 | ) 78 | ]; 79 | 80 | return [ 81 | new CachingTransformer($this, $sourceTransformers, $cachePathManager) 82 | ]; 83 | } 84 | } 85 | 86 | require __DIR__ . '/Intercept/before_mock.php'; 87 | -------------------------------------------------------------------------------- /src/AspectMock/Proxy/Anything.php: -------------------------------------------------------------------------------- 1 | className = $className; 24 | } 25 | 26 | public function __toString() 27 | { 28 | return "| Undefined | ".$this->className; 29 | } 30 | 31 | public function __get($key): Anything 32 | { 33 | return new Anything($this->className); 34 | } 35 | 36 | public function __set($key, $val) 37 | { 38 | } 39 | 40 | public function __call($method, $args): Anything 41 | { 42 | return new Anything($this->className); 43 | } 44 | 45 | public function offsetExists($offset): bool 46 | { 47 | return false; 48 | } 49 | 50 | public function offsetGet($offset): Anything 51 | { 52 | return new Anything($this->className); 53 | } 54 | 55 | 56 | public function offsetSet($offset, $value): void 57 | { 58 | } 59 | 60 | public function offsetUnset($offset): void 61 | { 62 | } 63 | 64 | public function current() 65 | { 66 | return null; 67 | } 68 | 69 | public function next(): void 70 | { 71 | } 72 | 73 | public function key() 74 | { 75 | return null; 76 | } 77 | 78 | public function valid(): bool 79 | { 80 | return false; 81 | } 82 | 83 | public function rewind(): void 84 | { 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/AspectMock/Proxy/AnythingClassProxy.php: -------------------------------------------------------------------------------- 1 | className = $className; 14 | $this->reflected = new ReflectionClass(Anything::class); 15 | } 16 | 17 | public function isDefined(): bool 18 | { 19 | return false; 20 | } 21 | 22 | public function construct(): Anything 23 | { 24 | return new Anything($this->className); 25 | } 26 | 27 | public function make(): Anything 28 | { 29 | return new Anything($this->className); 30 | } 31 | 32 | public function interfaces(): array 33 | { 34 | return array(); 35 | } 36 | 37 | public function hasMethod($method): bool 38 | { 39 | return false; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/AspectMock/Proxy/ClassProxy.php: -------------------------------------------------------------------------------- 1 | construct(); 24 | * $user->save(); 25 | * $userModel->verifyInvoked('tableName'); 26 | * $userModel->verifyInvoked('save'); 27 | * ``` 28 | * 29 | * You can get a class name of a proxy via `className` property. 30 | * 31 | * ```php 32 | * className; // UserModel 35 | * ``` 36 | * 37 | * Also, you can get the list of calls for a specific method. 38 | * 39 | * ```php 40 | * someMethod('arg1', 'arg2'); 43 | * $user->getCallsForMethod('someMethod') // [ ['arg1', 'arg2'] ] 44 | * ``` 45 | */ 46 | class ClassProxy extends Verifier 47 | { 48 | protected ReflectionClass $reflected; 49 | 50 | public function __construct($className) 51 | { 52 | $this->className = $className; 53 | $this->reflected = new ReflectionClass($className); 54 | } 55 | 56 | public function getCallsForMethod($method) 57 | { 58 | $calls = Registry::getClassCallsFor($this->className); 59 | return $calls[$method] ?? []; 60 | } 61 | 62 | /** 63 | * Returns true if class exists. 64 | * Returns false if class is not defined yet, and was declared via `test::spec`. 65 | */ 66 | public function isDefined(): bool 67 | { 68 | return true; 69 | } 70 | 71 | /** 72 | * Returns an array with all interface names of a class 73 | */ 74 | public function interfaces(): array 75 | { 76 | return $this->getRealClass()->getInterfaceNames(); 77 | } 78 | 79 | /** 80 | * Returns a name of the parent of a class. 81 | * 82 | * @return null 83 | */ 84 | public function parent() 85 | { 86 | $parent = $this->getRealClass()->getParentClass(); 87 | if ($parent) return $parent->name; 88 | 89 | return null; 90 | } 91 | 92 | /** 93 | * @param mixed $method 94 | */ 95 | public function hasMethod($method): bool 96 | { 97 | return $this->getRealClass()->hasMethod($method); 98 | } 99 | 100 | /** 101 | * @param mixed $property 102 | */ 103 | public function hasProperty($property): bool 104 | { 105 | return $this->getRealClass()->hasProperty($property); 106 | } 107 | 108 | /** 109 | * Returns array of all trait names of a class. 110 | */ 111 | public function traits(): array 112 | { 113 | return $this->getRealClass()->getTraitNames(); 114 | } 115 | 116 | private function getRealClass() 117 | { 118 | if (in_array('Go\Aop\Proxy', $this->reflected->getInterfaceNames())) { 119 | return $this->reflected->getParentClass(); 120 | } 121 | 122 | return $this->reflected; 123 | } 124 | 125 | /** 126 | * Creates an instance of a class via constructor. 127 | * 128 | * ```php 129 | * construct([ 131 | * 'name' => 'davert', 132 | * 'email' => 'davert@mail.ua' 133 | * ]); 134 | * ``` 135 | */ 136 | public function construct(): object 137 | { 138 | return $this->reflected->newInstanceArgs(func_get_args()); 139 | } 140 | 141 | /** 142 | * Creates a class instance without calling a constructor. 143 | * 144 | * ```php 145 | * make(); 147 | * ``` 148 | */ 149 | public function make(): object 150 | { 151 | return $this->reflected->newInstanceWithoutConstructor(); 152 | } 153 | 154 | public function __call($method, $args) 155 | { 156 | throw new Exception("Called {$this->className}->{$method}, but this is a proxy for a class definition.\nProbably you were trying to access an instance method.\nConstruct an instance from this class"); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/AspectMock/Proxy/FuncProxy.php: -------------------------------------------------------------------------------- 1 | verifyInvoked(); // true 22 | * $func->verifyInvoked(['hello']); // true 23 | * $func->verifyInvokedMultipleTimes(2); 24 | * $func->verifyNeverInvoked(['bye']); 25 | * ``` 26 | */ 27 | class FuncProxy 28 | { 29 | protected string $func; 30 | 31 | protected string $ns; 32 | 33 | protected string $fullFuncName; 34 | 35 | protected FuncVerifier $funcVerifier; 36 | 37 | public function __construct(string $namespace, string $func) 38 | { 39 | $this->func = $func; 40 | $this->ns = $namespace; 41 | $this->fullFuncName = $namespace . '/' . $func; 42 | $this->funcVerifier = new FuncVerifier($namespace); 43 | } 44 | 45 | public function verifyInvoked(array $params = null): void 46 | { 47 | $this->funcVerifier->verifyInvoked($this->func, $params); 48 | } 49 | 50 | public function verifyInvokedOnce(array $params = null): void 51 | { 52 | $this->funcVerifier->verifyInvokedMultipleTimes($this->func, 1, $params); 53 | } 54 | 55 | public function verifyNeverInvoked(array $params = null): void 56 | { 57 | $this->funcVerifier->verifyNeverInvoked($this->func, $params); 58 | } 59 | 60 | public function verifyInvokedMultipleTimes(int $times, array $params = null): void 61 | { 62 | $this->funcVerifier->verifyInvokedMultipleTimes($this->func, $times, $params); 63 | } 64 | 65 | /** 66 | * Executes mocked function with provided parameters. 67 | * 68 | * @return mixed 69 | */ 70 | public function __invoke() 71 | { 72 | return call_user_func_array($this->ns .'\\'.$this->func, func_get_args()); 73 | } 74 | 75 | public function getCallsForMethod(string $func): array 76 | { 77 | return Registry::getFuncCallsFor($this->ns . '\\' . $func); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/AspectMock/Proxy/FuncVerifier.php: -------------------------------------------------------------------------------- 1 | ns = $namespace; 16 | } 17 | 18 | protected function callSyntax($method): string 19 | { 20 | return ''; 21 | } 22 | 23 | public function getCallsForMethod($func) 24 | { 25 | return Registry::getFuncCallsFor($this->ns . '\\' . $func); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/AspectMock/Proxy/InstanceProxy.php: -------------------------------------------------------------------------------- 1 | getObject(); // true 22 | * 23 | * ``` 24 | * 25 | * Contains verification methods and `class` property that points to `ClassProxy`. 26 | * 27 | * ``` php 28 | * 'davert']); 30 | * $user = test::double(new User); 31 | * // now $user is a proxy class of user 32 | * $this->assertSame('davert', $user->getName()); // success 33 | * $user->verifyInvoked('getName'); // success 34 | * $this->assertInstanceOf('User', $user); // fail 35 | * ``` 36 | * 37 | * A `class` property allows to verify method calls to any instance of this class. 38 | * Constains a **ClassVerifier** object. 39 | * 40 | * ``` php 41 | * class->hasMethod('save'); 44 | * $user->setName('davert'); 45 | * $user->class->verifyInvoked('setName'); 46 | * ``` 47 | * Also, you can get the list of calls for a specific method. 48 | * 49 | * ```php 50 | * someMethod('arg1', 'arg2'); 53 | * $user->getCallsForMethod('someMethod') // [ ['arg1', 'arg2'] ] 54 | * ``` 55 | */ 56 | class InstanceProxy extends Verifier 57 | { 58 | protected $instance; 59 | 60 | public function __construct($object) 61 | { 62 | $this->instance = $object; 63 | $this->className = get_class($object); 64 | } 65 | 66 | protected function callSyntax($method): string 67 | { 68 | return '->'; 69 | } 70 | 71 | /** 72 | * Returns a real object that is proxified. 73 | * 74 | * @return mixed 75 | */ 76 | public function getObject() 77 | { 78 | return $this->instance; 79 | } 80 | 81 | public function getCallsForMethod($method) 82 | { 83 | $calls = Registry::getInstanceCallsFor($this->instance); 84 | return $calls[$method] ?? []; 85 | } 86 | 87 | // proxify calls to the methods 88 | public function __call($method, $args) 89 | { 90 | if (method_exists($this->instance, $method)) 91 | { 92 | // Is the method expecting any argument passed by reference? 93 | $passed_args = array(); 94 | $reflMethod = new ReflectionMethod($this->instance, $method); 95 | $params = $reflMethod->getParameters(); 96 | 97 | for($i = 0; $i < count($params); $i++) 98 | { 99 | if(!isset($args[$i])) 100 | { 101 | break; 102 | } 103 | 104 | if($params[$i]->isPassedByReference()) 105 | { 106 | $passed_args[] = &$args[$i]; 107 | } 108 | else 109 | { 110 | $passed_args[] = $args[$i]; 111 | } 112 | } 113 | 114 | return call_user_func_array([$this->instance, $method], $passed_args); 115 | } 116 | 117 | if (method_exists($this->instance, '__call')) { 118 | return call_user_func([$this->instance, '__call'], $method, $args); 119 | } 120 | } 121 | 122 | public function __get($property) 123 | { 124 | if ($property === 'class') { 125 | return $this->class = new ClassProxy($this->className); 126 | } 127 | if (method_exists($this->instance, '__get')) { 128 | return call_user_func([$this->instance, '__get'], $property); 129 | } 130 | return $this->instance->$property; 131 | } 132 | 133 | public function __set($property, $value) 134 | { 135 | $this->instance->$property = $value; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/AspectMock/Proxy/Verifier.php: -------------------------------------------------------------------------------- 1 | className,$method) 35 | ? '::' 36 | : '->'; 37 | } 38 | 39 | protected function onlyExpectedArguments($expectedParams, $passedArgs) 40 | { 41 | return empty($expectedParams) ? 42 | $passedArgs : 43 | array_slice($passedArgs, 0, count($expectedParams)); 44 | } 45 | 46 | /** 47 | * Verifies a method was invoked at least once. 48 | * In second argument you can specify with which params method expected to be invoked; 49 | * 50 | * ``` php 51 | * verifyInvoked('save'); 53 | * $user->verifyInvoked('setName',['davert']); 54 | * 55 | * ``` 56 | */ 57 | public function verifyInvoked(string $name, $params = null) 58 | { 59 | $calls = $this->getCallsForMethod($name); 60 | $separator = $this->callSyntax($name); 61 | 62 | if (empty($calls)) throw new ExpectationFailedException(sprintf($this->invokedFail, $this->className.$separator.$name, '')); 63 | 64 | if (is_array($params)) { 65 | foreach ($calls as $args) { 66 | if ($this->onlyExpectedArguments($params, $args) === $params) return; 67 | } 68 | 69 | $params = ArgumentsFormatter::toString($params); 70 | $gotParams = ArgumentsFormatter::toString($calls[0]); 71 | throw new ExpectationFailedException(sprintf($this->invokedFail, $this->className.$separator.$name.sprintf('(%s)', $params), $this->className.$separator.$name.sprintf('(%s)', $gotParams))); 72 | } elseif (is_callable($params)) { 73 | $params($calls); 74 | } 75 | } 76 | 77 | /** 78 | * Verifies that method was invoked only once. 79 | */ 80 | public function verifyInvokedOnce(string $name, $params = null): void 81 | { 82 | $this->verifyInvokedMultipleTimes($name, 1, $params); 83 | } 84 | 85 | /** 86 | * Verifies that method was called exactly $times times. 87 | * 88 | * ``` php 89 | * verifyInvokedMultipleTimes('save',2); 91 | * $user->verifyInvokedMultipleTimes('dispatchEvent',3,['before_validate']); 92 | * $user->verifyInvokedMultipleTimes('dispatchEvent',4,['after_save']); 93 | * ``` 94 | * 95 | * @throws ExpectationFailedException 96 | */ 97 | public function verifyInvokedMultipleTimes(string $name, int $times, $params = null) 98 | { 99 | if ($times == 0) return $this->verifyNeverInvoked($name, $params); 100 | 101 | $calls = $this->getCallsForMethod($name); 102 | $separator = $this->callSyntax($name); 103 | 104 | if (empty($calls)) throw new ExpectationFailedException(sprintf($this->notInvokedMultipleTimesFail, $this->className.$separator.$name, $times)); 105 | 106 | if (is_array($params)) { 107 | $equals = 0; 108 | foreach ($calls as $args) { 109 | if ($this->onlyExpectedArguments($params, $args) === $params) { 110 | ++$equals; 111 | } 112 | } 113 | 114 | if ($equals == $times) { 115 | Assert::assertTrue(true); 116 | return; 117 | } 118 | 119 | $params = ArgumentsFormatter::toString($params); 120 | throw new ExpectationFailedException(sprintf($this->invokedMultipleTimesFail, $this->className.$separator.$name.sprintf('(%s)', $params), $times, $equals)); 121 | } elseif (is_callable($params)) { 122 | $params($calls); 123 | } 124 | 125 | $num_calls = count($calls); 126 | if ($num_calls != $times) throw new ExpectationFailedException(sprintf($this->invokedMultipleTimesFail, $this->className.$separator.$name, $times, $num_calls)); 127 | 128 | Assert::assertTrue(true); 129 | } 130 | 131 | /** 132 | * Verifies that method was not called. 133 | * In second argument with which arguments is not expected to be called. 134 | * 135 | * ``` php 136 | * setName('davert'); 138 | * $user->verifyNeverInvoked('setName'); // fail 139 | * $user->verifyNeverInvoked('setName',['davert']); // fail 140 | * $user->verifyNeverInvoked('setName',['bob']); // success 141 | * $user->verifyNeverInvoked('setName',[]); // success 142 | * ``` 143 | * 144 | * @throws ExpectationFailedException 145 | */ 146 | public function verifyNeverInvoked(string $name, $params = null) 147 | { 148 | $calls = $this->getCallsForMethod($name); 149 | $separator = $this->callSyntax($name); 150 | 151 | if (is_array($params)) { 152 | if (empty($calls)) { 153 | Assert::assertTrue(true); 154 | return; 155 | } 156 | 157 | foreach ($calls as $args) { 158 | if ($this->onlyExpectedArguments($params, $args) === $params) { 159 | throw new ExpectationFailedException(sprintf($this->neverInvoked, $this->className)); 160 | } 161 | } 162 | 163 | Assert::assertTrue(true); 164 | return; 165 | } 166 | 167 | if (count($calls) > 0) { 168 | throw new ExpectationFailedException(sprintf($this->neverInvoked, $this->className.$separator.$name)); 169 | } 170 | 171 | Assert::assertTrue(true); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/AspectMock/Test.php: -------------------------------------------------------------------------------- 1 | 'davert']); 44 | * $user->getName() // => davert 45 | * $user->verifyInvoked('getName'); // => success 46 | * $user->getObject() // => returns instance of User, i.e. real, not proxified object 47 | * 48 | * # with closure 49 | * $user = test::double(new User, ['getName' => function() { return $this->login; }]); 50 | * $user->login = 'davert'; 51 | * $user->getName(); // => davert 52 | * 53 | * # on a class 54 | * $ar = test::double('ActiveRecord', ['save' => null]); 55 | * $user = new User; 56 | * $user->name = 'davert'; 57 | * $user->save(); // passes to ActiveRecord->save() and does not insert any SQL. 58 | * $ar->verifyInvoked('save'); // true 59 | * 60 | * # on static method call 61 | * User::tableName(); // 'users' 62 | * $user = test::double('User', ['tableName' => 'fake_users']); 63 | * User::tableName(); // 'fake_users' 64 | * $user->verifyInvoked('tableName'); // success 65 | * 66 | * # append declaration 67 | * $user = new User; 68 | * test::double($user, ['getName' => 'davert']); 69 | * test::double($user, ['getEmail' => 'davert@mail.ua']); 70 | * $user->getName(); // => 'davert' 71 | * $user->getEmail(); => 'davert@mail.ua' 72 | * 73 | * # create an instance of mocked class 74 | * test::double('User')->construct(['name' => 'davert']); // via constructor 75 | * test::double('User')->make(); // without calling constructor 76 | * 77 | * # stub for magic method 78 | * test::double('User', ['findByUsernameAndPasswordAndEmail' => false]); 79 | * User::findByUsernameAndPasswordAndEmail(); // null 80 | * 81 | * # stub for method of parent class 82 | * # if User extends ActiveRecord 83 | * test::double('ActiveRecord', ['save' => false]); 84 | * $user = new User(['name' => 'davert']); 85 | * $user->save(); // false 86 | * 87 | * ``` 88 | * 89 | * @api 90 | * @param string|object $classOrObject 91 | * @param array $params [ 'methodName' => 'returnValue' ] 92 | * @throws Exception 93 | * @return Verifier|Proxy\ClassProxy|Proxy\InstanceProxy 94 | */ 95 | public static function double($classOrObject, array $params = array()) 96 | { 97 | $classOrObject = Registry::getRealClassOrObject($classOrObject); 98 | if (is_string($classOrObject)) { 99 | if (!class_exists($classOrObject)) { 100 | throw new Exception("Class $classOrObject not loaded.\nIf you want to test undefined class use 'test::spec' method"); 101 | } 102 | 103 | Core\Registry::registerClass($classOrObject, $params); 104 | return new Proxy\ClassProxy($classOrObject); 105 | } 106 | if (is_object($classOrObject)) { 107 | Core\Registry::registerObject($classOrObject, $params); 108 | return new Proxy\InstanceProxy($classOrObject); 109 | } 110 | return null; 111 | } 112 | 113 | /** 114 | * If you follow TDD/BDD practices a test should be written before the class is defined. 115 | * If you would call undefined class in a test, a fatal error will be triggered. 116 | * Instead you can use `test::spec` method that will create a proxy for an undefined class. 117 | * 118 | * ``` php 119 | * defined(); // false 122 | * ``` 123 | * 124 | * You can create instances of undefined classes and play with them: 125 | * 126 | * ``` php 127 | * construct(); 129 | * $user->setName('davert'); 130 | * $user->setNumPosts(count($user->getPosts())); 131 | * $this->assertSame('davert', $user->getName()); // fail 132 | * ``` 133 | * 134 | * The test will be executed normally and will fail at the first assertion. 135 | * 136 | * `test::spec()->construct` creates an instance of `AspectMock\Proxy\Anything` 137 | * which tries not to cause errors whatever you try to do with it. 138 | * 139 | * ``` php 140 | * construct(); 142 | * $user->can()->be->called()->anything(); 143 | * $user->can['be used as array']; 144 | * foreach ($user->names as $name) { 145 | * $name->canBeIterated(); 146 | * } 147 | * ``` 148 | * 149 | * None of those calls will trigger an error in your test. 150 | * Thus, you can write a valid test before the class is declared. 151 | * 152 | * If class is already defined, `test::spec` will act as `test::double`. 153 | * 154 | * @api 155 | * @param string|object $classOrObject 156 | * @param array $params 157 | * @return Verifier|Proxy\ClassProxy|Proxy\InstanceProxy 158 | */ 159 | public static function spec($classOrObject, array $params = array()) 160 | { 161 | if (is_object($classOrObject)) return self::double($classOrObject, $params); 162 | if (class_exists($classOrObject)) return self::double($classOrObject, $params); 163 | 164 | return new AnythingClassProxy($classOrObject); 165 | } 166 | 167 | /** 168 | * Replaces all methods in a class with dummies, except those specified in the `$only` param. 169 | * 170 | * ``` php 171 | * 'jon']); 173 | * test::methods($user, ['getName']); 174 | * $user->setName('davert'); // not invoked 175 | * $user->getName(); // jon 176 | * ``` 177 | * 178 | * You can create a dummy without a constructor with all methods disabled: 179 | * 180 | * ``` php 181 | * make(); 183 | * test::methods($user, []); 184 | * ``` 185 | * 186 | * @api 187 | * @param string|object $classOrObject 188 | * @param string[] $only 189 | * @return Verifier|Proxy\ClassProxy|Proxy\InstanceProxy 190 | * @throws Exception 191 | */ 192 | public static function methods($classOrObject, array $only = array()) 193 | { 194 | $classOrObject = Registry::getRealClassOrObject($classOrObject); 195 | if (is_object($classOrObject)) { 196 | $reflected = new ReflectionClass(get_class($classOrObject)); 197 | } else { 198 | if (!class_exists($classOrObject)) throw new Exception("Class $classOrObject not defined."); 199 | $reflected = new ReflectionClass($classOrObject); 200 | } 201 | $methods = $reflected->getMethods(ReflectionMethod::IS_PUBLIC); 202 | $params = array(); 203 | foreach ($methods as $m) { 204 | if ($m->isConstructor()) continue; 205 | if ($m->isDestructor()) continue; 206 | if (in_array($m->name, $only)) continue; 207 | $params[$m->name] = null; 208 | } 209 | return self::double($classOrObject, $params); 210 | } 211 | 212 | /** 213 | * Replaces function in provided namespace with user-defined function or value that function returns. 214 | * Function is restored to original on cleanup. 215 | * 216 | * ```php 217 | * verifyInvoked(); 240 | * $func->verifyInvokedOnce(['Y']); 241 | * ``` 242 | * 243 | * @param mixed $body whatever a function might return or Callable substitute 244 | * @return Proxy\FuncProxy 245 | */ 246 | public static function func(string $namespace, string $functionName, $body) 247 | { 248 | Core\Registry::registerFunc($namespace, $functionName, $body); 249 | return new Proxy\FuncProxy($namespace, $functionName); 250 | } 251 | 252 | /** 253 | * Clears test doubles registry. 254 | * Should be called between tests. 255 | * 256 | * ``` php 257 | * add('AspectMock', __DIR__ . '/../src'); 9 | $loader->add('demo', __DIR__ . '/_data'); 10 | $loader->register(); 11 | 12 | $kernel = Kernel::getInstance(); 13 | $kernel->init([ 14 | 'cacheDir' => __DIR__.'/_data/cache', 15 | 'includePaths' => [__DIR__.'/_data/demo'], 16 | 'interceptFunctions' => true 17 | ]); 18 | -------------------------------------------------------------------------------- /tests/_data/autoload.php: -------------------------------------------------------------------------------- 1 | add('demo',__DIR__); 4 | //$loader->register(); 5 | require_once __DIR__.'/demo/UserModel.php'; 6 | require_once __DIR__.'/demo/UserService.php'; 7 | require_once __DIR__.'/demo/AdminUserModel.php'; 8 | require_once __DIR__.'/demo/WorkingTrait.php'; -------------------------------------------------------------------------------- /tests/_data/demo.php: -------------------------------------------------------------------------------- 1 | add('AspectMock', __DIR__ . '/../../src'); 4 | $loader->add('demo', __DIR__ ); 5 | $loader->register(); 6 | 7 | $kernel = \AspectMock\Kernel::getInstance(); 8 | $kernel->init([ 9 | 'debug' => true, 10 | 'cacheDir' => __DIR__.'/cache', 11 | ]); 12 | 13 | $user = new demo\UserModel; 14 | $user->save(); -------------------------------------------------------------------------------- /tests/_data/demo/AdminUserModel.php: -------------------------------------------------------------------------------- 1 | name = "Admin_111"; 12 | parent::save(); 13 | } 14 | 15 | 16 | 17 | } -------------------------------------------------------------------------------- /tests/_data/demo/MegaClass.php: -------------------------------------------------------------------------------- 1 | 18 | * Move forward to next element 19 | * @link http://php.net/manual/en/iterator.next.php 20 | * @return void Any returned value is ignored. 21 | */ 22 | public function next() 23 | { 24 | // TODO: Implement next() method. 25 | } 26 | 27 | /** 28 | * (PHP 5 >= 5.0.0)
29 | * Return the key of the current element 30 | * @link http://php.net/manual/en/iterator.key.php 31 | * @return mixed scalar on success, or null on failure. 32 | */ 33 | public function key() 34 | { 35 | // TODO: Implement key() method. 36 | } 37 | 38 | /** 39 | * (PHP 5 >= 5.0.0)
40 | * Checks if current position is valid 41 | * @link http://php.net/manual/en/iterator.valid.php 42 | * @return boolean The return value will be casted to boolean and then evaluated. 43 | * Returns true on success or false on failure. 44 | */ 45 | public function valid() 46 | { 47 | // TODO: Implement valid() method. 48 | } 49 | 50 | /** 51 | * (PHP 5 >= 5.0.0)
52 | * Rewind the Iterator to the first element 53 | * @link http://php.net/manual/en/iterator.rewind.php 54 | * @return void Any returned value is ignored. 55 | */ 56 | public function rewind() 57 | { 58 | // TODO: Implement rewind() method. 59 | } 60 | } -------------------------------------------------------------------------------- /tests/_data/demo/TraitedClass1.php: -------------------------------------------------------------------------------- 1 | $value) 24 | { 25 | $this->$key = $value; 26 | } 27 | } 28 | 29 | /** 30 | * @param mixed $name 31 | */ 32 | public function setName($name) 33 | { 34 | $this->name = $name; 35 | return $this; 36 | } 37 | 38 | /** 39 | * @return mixed 40 | */ 41 | public function getName() 42 | { 43 | return $this->name; 44 | } 45 | 46 | /** 47 | * @param mixed $name 48 | */ 49 | public function setNameAndInfo($name, $info) 50 | { 51 | $this->name = $name; 52 | $this->info = $info; 53 | return $this; 54 | } 55 | 56 | /** 57 | * @return mixed 58 | */ 59 | public function getNameAndInfo() 60 | { 61 | return array($this->name, $this->info); 62 | } 63 | 64 | /** 65 | * @param array $info 66 | */ 67 | public function setInfo($info) 68 | { 69 | $this->info = $info; 70 | return $this; 71 | } 72 | 73 | /** 74 | * @return array 75 | */ 76 | public function getInfo($info) 77 | { 78 | return $this->info; 79 | } 80 | 81 | function save() 82 | { 83 | throw new \PHPUnit\Framework\AssertionFailedError("I should not be called"); 84 | } 85 | 86 | public function __call($name, $args = array()) 87 | { 88 | if ($name == 'renameUser') return 'David Blane'; 89 | } 90 | 91 | public static function __callStatic($name, $args) 92 | { 93 | if ($name == 'defaultRole') return "member"; 94 | } 95 | 96 | public function dump() 97 | { 98 | return file_put_contents(\Codeception\Configuration::logDir().'user.txt',$this->name); 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /tests/_data/demo/UserService.php: -------------------------------------------------------------------------------- 1 | save(); 10 | } 11 | 12 | public function updateName(UserModel $user) 13 | { 14 | $user->setName("Current User"); 15 | $user->save(); 16 | } 17 | 18 | public function renameUser(UserModel $user, $name) 19 | { 20 | $user->renameUser($name); 21 | $user->save(); 22 | } 23 | 24 | public static function renameStatic(UserModel $user, $name) 25 | { 26 | $user->renameUser($name); 27 | $user->save(); 28 | } 29 | 30 | public function __call($name, $args) 31 | { 32 | if ($name == 'rename') { 33 | return 'David Blane'; 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /tests/_data/demo/WorkingTrait.php: -------------------------------------------------------------------------------- 1 | 'davert']); 15 | $this->assertSame('davert', $user->getName()); 16 | } 17 | 18 | public function testUserService() 19 | { 20 | $this->expectException(AssertionFailedError::class); 21 | $service = new UserService(); 22 | $service->create(['name' => 'davert']); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/unit/ClassProxyTest.php: -------------------------------------------------------------------------------- 1 | isDefined())->true(); 19 | verify($class->hasMethod('setName'))->true(); 20 | verify($class->hasMethod('setNothing'))->false(); 21 | verify($class->hasProperty('name'))->true(); 22 | verify($class->hasProperty('otherName'))->false(); 23 | verify($class->traits())->empty(); 24 | verify($class->interfaces())->empty(); 25 | verify($class->parent())->null(); 26 | } 27 | 28 | public function testMegaClassValidations() 29 | { 30 | $class = test::double('demo\MegaClass'); 31 | /** @var $class ClassProxy **/ 32 | verify($class->isDefined())->true(); 33 | verify($class->hasMethod('setName'))->false(); 34 | verify($class->traits())->arrayContains('Codeception\Specify'); 35 | verify($class->interfaces())->arrayContains('Iterator'); 36 | verify($class->parent())->equals('stdClass'); 37 | } 38 | 39 | public function testUndefinedClass() 40 | { 41 | $this->expectException('Exception'); 42 | test::double('MyUndefinedClass'); 43 | } 44 | 45 | public function testInstanceCreation() 46 | { 47 | $this->class = test::double('demo\UserModel'); 48 | 49 | $this->specify('instance can be created from a class proxy', function() { 50 | $user = $this->class->construct(['name' => 'davert']); 51 | verify($user->getName())->equals('davert'); 52 | $this->assertInstanceOf('demo\UserModel', $user); 53 | }); 54 | 55 | $this->specify('instance can be created without constructor', function() { 56 | $user = $this->class->make(); 57 | $this->assertInstanceOf('demo\UserModel', $user); 58 | }); 59 | } 60 | 61 | public function testClassWithTraits() { 62 | // if a trait is used by more than one doubled class, when BeforeMockTransformer 63 | // runs on the second class it will see the trait's methods as being a part of 64 | // the class itself, and try to inject its code into the class, rather than the 65 | // trait. in failure mode, this test will result in: 66 | // Parse error: syntax error, unexpected 'if' (T_IF), expecting function (T_FUNCTION) 67 | // in [...]/TraitedModel2.php 68 | 69 | $unused = test::double('demo\TraitedClass1'); // this model uses `TraitedModelTrait` 70 | $class = test::double('demo\TraitedClass2'); // so does this one 71 | /** @var $class ClassProxy **/ 72 | verify($class->isDefined())->true(); 73 | verify($class->hasMethod('method1InTrait'))->true(); 74 | verify($class->hasMethod('methodInClass'))->true(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/unit/FunctionInjectorTest.php: -------------------------------------------------------------------------------- 1 | funcInjector = new FunctionInjector('demo', 'strlen'); 22 | $this->funcOptionalParameterInjector = new FunctionInjector('demo', 'explode'); 23 | $this->funcReferencedParameterInjector = new FunctionInjector('demo', 'preg_match'); 24 | test::clean(); 25 | } 26 | 27 | public function testTemplate() 28 | { 29 | $php = $this->funcInjector->getPHP(); 30 | verify($php)->stringContainsString("function strlen()"); 31 | verify($php)->stringContainsString("return call_user_func_array('strlen', func_get_args());"); 32 | } 33 | 34 | public function testReferencedParameterTemplate() 35 | { 36 | $php = $this->funcReferencedParameterInjector->getPHP(); 37 | verify($php)->stringContainsString("function preg_match(\$p0, \$p1, &\$p2=NULL, \$p3=NULL, \$p4=NULL)"); 38 | verify($php)->stringContainsString("case 5: \$args = [\$p0, \$p1, &\$p2, \$p3, \$p4]; break;"); 39 | verify($php)->stringContainsString("case 4: \$args = [\$p0, \$p1, &\$p2, \$p3]; break;"); 40 | verify($php)->stringContainsString("case 3: \$args = [\$p0, \$p1, &\$p2]; break;"); 41 | verify($php)->stringContainsString("case 2: \$args = [\$p0, \$p1]; break;"); 42 | verify($php)->stringContainsString("case 1: \$args = [\$p0]; break;"); 43 | verify($php)->stringContainsString("return call_user_func_array('preg_match', \$args);"); 44 | } 45 | 46 | public function testSave() 47 | { 48 | $this->funcInjector->save(); 49 | exec('php -l '.$this->funcInjector->getFileName(), $output, $code); 50 | verify($code)->equals(0); 51 | codecept_debug($this->funcInjector->getPHP()); 52 | } 53 | 54 | public function testLoadFunc() 55 | { 56 | $this->funcInjector->save(); 57 | codecept_debug($this->funcInjector->getFileName()); 58 | $this->funcInjector->inject(); 59 | verify(strlen('hello'))->equals(5); 60 | } 61 | 62 | public function testReimplementFunc() 63 | { 64 | test::func('demo', 'strlen', 10); 65 | verify(strlen('hello'))->equals(10); 66 | } 67 | 68 | public function testFuncReturnsNull() 69 | { 70 | test::func('demo', 'strlen', null); 71 | verify(strlen('hello'))->equals(null); 72 | } 73 | 74 | public function testVerifier() 75 | { 76 | $func = test::func('demo', 'strlen', 10); 77 | verify(strlen('hello'))->equals(10); 78 | $func->verifyInvoked(); 79 | $func->verifyInvoked(['hello']); 80 | $func->verifyInvokedOnce(); 81 | $func->verifyInvokedOnce(['hello']); 82 | $func->verifyInvokedMultipleTimes(1, ['hello']); 83 | $func->verifyNeverInvoked(['hee']); 84 | } 85 | 86 | public function testVerifierFullyQualifiedNamespace() 87 | { 88 | $func = test::func('\demo', 'strlen', 10); 89 | verify(strlen('hello'))->equals(10); 90 | $func->verifyInvoked(); 91 | $func->verifyInvoked(['hello']); 92 | $func->verifyInvokedOnce(); 93 | $func->verifyInvokedOnce(['hello']); 94 | $func->verifyInvokedMultipleTimes(1, ['hello']); 95 | $func->verifyNeverInvoked(['hee']); 96 | } 97 | 98 | /** 99 | * @test 100 | */ 101 | public function testFailedVerification() 102 | { 103 | $this->expectException(ExpectationFailedException::class); 104 | $func = test::func('demo', 'strlen', function() { return 10; }); 105 | verify(strlen('hello'))->equals(10); 106 | $func->verifyNeverInvoked(); 107 | } 108 | 109 | public function testReferencedParameter() 110 | { 111 | $func = test::func('\demo', 'preg_match', 10); 112 | verify(preg_match('@[0-9]+@', '1234', $match))->equals(10); 113 | test::clean(); 114 | verify(preg_match('@[0-9]+@', '1234#', $match))->equals(1); 115 | verify($match[0])->equals('1234'); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/unit/MockFailedTest.php: -------------------------------------------------------------------------------- 1 | expectException('PHPUnit\Framework\ExpectationFailedException'); 18 | } 19 | 20 | protected function _tearDown() 21 | { 22 | double::clean(); 23 | } 24 | 25 | protected function user(): InstanceProxy 26 | { 27 | $user = new UserModel(); 28 | double::registerObject($user); 29 | return new InstanceProxy($user); 30 | } 31 | 32 | protected function userProxy(): ClassProxy 33 | { 34 | $userProxy = new ClassProxy('demo\UserModel'); 35 | double::registerClass('demo\UserModel'); 36 | return $userProxy; 37 | } 38 | 39 | public function testInstanceInvoked() 40 | { 41 | $this->user()->verifyInvoked('setName'); 42 | } 43 | 44 | public function testInstanceInvokedWithoutParams() 45 | { 46 | $user = $this->user(); 47 | $user->setName('davert'); 48 | $user->verifyInvoked('setName',[]); 49 | } 50 | 51 | public function testInstanceInvokedMultipleTimes() 52 | { 53 | $user = $this->user(); 54 | $user->setName('davert'); 55 | $user->setName('jon'); 56 | $user->verifyInvokedMultipleTimes('setName',3); 57 | } 58 | 59 | public function testInstanceInvokedMultipleTimesWithoutParams() 60 | { 61 | $user = $this->user(); 62 | $user->setName('davert'); 63 | $user->setName('jon'); 64 | $user->verifyInvokedMultipleTimes('setName',2,['davert']); 65 | } 66 | 67 | public function testClassMethodFails() 68 | { 69 | $userProxy = $this->userProxy(); 70 | UserModel::tableName(); 71 | UserModel::tableName(); 72 | $userProxy->verifyInvokedOnce('tableName'); 73 | } 74 | 75 | public function testClassMethodNeverInvokedFails() 76 | { 77 | $user = new UserModel(); 78 | $userProxy = $this->userProxy(); 79 | $user->setName('davert'); 80 | $userProxy->verifyNeverInvoked('setName'); 81 | 82 | } 83 | 84 | public function testClassMethodInvokedMultipleTimes() 85 | { 86 | $user = new UserModel(); 87 | $userProxy = $this->userProxy(); 88 | $user->setName('davert'); 89 | $user->setName('bob'); 90 | $userProxy->verifyInvokedMultipleTimes('setName',2,['davert']); 91 | } 92 | 93 | public function testClassMethodInvoked() 94 | { 95 | $user = new UserModel(); 96 | $userProxy = $this->userProxy(); 97 | $user->setName(1111); 98 | $userProxy->verifyInvoked('setName',[2222]); 99 | } 100 | 101 | public function testAnythingFail() 102 | { 103 | $anyProxy = new AnythingClassProxy('demo\UserModel'); 104 | $any = $anyProxy->construct(); 105 | $any->hello(); 106 | $anyProxy->verifyInvoked('hello'); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/unit/MockTest.php: -------------------------------------------------------------------------------- 1 | setName('davert'); 27 | 28 | $this->specify('setName() was invoked', function() use ($user) { 29 | $user->verifyInvoked('setName'); 30 | $user->verifyInvoked('setName',['davert']); 31 | $user->verifyInvokedMultipleTimes('setName',1); 32 | $user->verifyInvokedMultipleTimes('setName',1,['davert']); 33 | $user->verifyNeverInvoked('setName',['bugoga']); 34 | }); 35 | 36 | $this->specify('save() was not invoked', function() use ($user) { 37 | $user->verifyNeverInvoked('save'); 38 | $user->verifyNeverInvoked('save',['params']); 39 | }); 40 | } 41 | 42 | public function testVerifyClassMethods() 43 | { 44 | double::registerClass('demo\UserModel',['save' => null]); 45 | $user = new ClassProxy('demo\UserModel'); 46 | 47 | $service = new UserService(); 48 | $service->create(array('name' => 'davert')); 49 | $user->verifyInvoked('save'); 50 | $user->verifyInvoked('save',[]); 51 | $user->verifyInvokedMultipleTimes('save',1); 52 | $user->verifyNeverInvoked('getName'); 53 | } 54 | 55 | public function testVerifyStaticMethods() 56 | { 57 | double::registerClass('demo\UserModel'); 58 | $user = new ClassProxy('demo\UserModel'); 59 | UserModel::tableName(); 60 | $user->verifyInvoked('tableName'); 61 | } 62 | 63 | public function testVerifyThatWasCalledWithParameters() 64 | { 65 | $user = new UserModel(); 66 | double::registerObject($user); 67 | $user = new InstanceProxy($user); 68 | $user->setName('davert'); 69 | $user->setName('jon'); 70 | $user->verifyInvokedOnce('setName',['davert']); 71 | } 72 | 73 | public function testVerifyClassMethodCalled() 74 | { 75 | $user = new UserModel(); 76 | $userProxy = new ClassProxy('demo\UserModel'); 77 | double::registerClass('demo\UserModel'); 78 | $user->setName('davert'); 79 | $user->setName('jon'); 80 | $userProxy->verifyInvokedMultipleTimes('setName',2); 81 | $userProxy->verifyInvokedOnce('setName',['jon']); 82 | $userProxy->verifyNeverInvoked('save'); 83 | $userProxy->verifyNeverInvoked('setName',['bob']); 84 | verify($user->getName())->equals('jon'); 85 | } 86 | 87 | /** 88 | * Allow verifying extended methods. 89 | * 90 | * Given: 91 | * 92 | * 93 | * 100 | * 101 | * Verification: 102 | * 103 | * 104 | * super(); 113 | * 114 | * // Will pass 115 | * $parentProxy->verifyInvoked('super'); 116 | * $childProxy->verifyInvoked('super'); 117 | * 118 | */ 119 | public function testVerifyClassInheritedMethodCalled() 120 | { 121 | $adminUser = new AdminUserModel(); 122 | 123 | double::registerClass(UserModel::class); 124 | double::registerClass(AdminUserModel::class); 125 | 126 | $userProxy = new ClassProxy(UserModel::class); 127 | $adminUserProxy = new ClassProxy(AdminUserModel::class); 128 | 129 | $adminUser->getName(); 130 | 131 | $userProxy->verifyInvokedOnce('getName'); 132 | $adminUserProxy->verifyInvokedOnce('getName'); 133 | } 134 | } -------------------------------------------------------------------------------- /tests/unit/StubTest.php: -------------------------------------------------------------------------------- 1 | null]); 21 | $user = new UserModel(); 22 | $user->save(); 23 | } 24 | 25 | public function testSaveAgain() 26 | { 27 | double::registerClass('\demo\UserModel', ['save' => "saved!"]); 28 | $user = new UserModel(); 29 | $saved = $user->save(); 30 | $this->assertSame('saved!', $saved); 31 | } 32 | 33 | public function testCallback() 34 | { 35 | double::registerClass('\demo\UserModel', ['save' => function () { return $this->name; }]); 36 | $user = new UserModel(['name' => 'davert']); 37 | $name = $user->save(); 38 | $this->assertSame('davert', $name); 39 | 40 | } 41 | 42 | public function testBindSelfCallback() 43 | { 44 | double::registerClass('\demo\UserModel', ['getTopSecret' => function () { 45 | return UserModel::$topSecret; 46 | }]); 47 | $topSecret = UserModel::getTopSecret(); 48 | $this->assertSame('awesome', $topSecret); 49 | } 50 | 51 | public function testObjectInstance() 52 | { 53 | $user = new UserModel(['name' => 'davert']); 54 | double::registerObject($user,['save' => null]); 55 | $user->save(); 56 | } 57 | 58 | public function testStaticAccess() 59 | { 60 | $this->assertSame('users', UserModel::tableName()); 61 | double::registerClass('\demo\UserModel', ['tableName' => 'my_users']); 62 | $this->assertSame('my_users', UserModel::tableName()); 63 | } 64 | 65 | public function testInheritance() 66 | { 67 | double::registerClass('\demo\UserModel', ['save' => false]); 68 | $admin = new AdminUserModel(); 69 | $admin->save(); 70 | $this->assertSame('Admin_111', $admin->getName()); 71 | } 72 | 73 | public function testMagic() 74 | { 75 | double::registerClass('\demo\UserService', ['rename' => 'David Copperfield']); 76 | $admin = new UserService(); 77 | $this->assertSame('David Copperfield', $admin->rename()); 78 | 79 | } 80 | 81 | public function testMagicOfInheritedClass() 82 | { 83 | double::registerClass('\demo\AdminUserModel', ['renameUser' => 'David Copperfield']); 84 | $admin = new AdminUserModel(); 85 | $this->assertSame('David Copperfield', $admin->renameUser()); 86 | } 87 | 88 | public function testMagicStaticInherited() 89 | { 90 | double::registerClass('\demo\AdminUserModel', ['defaultRole' => 'admin']); 91 | $this->assertSame('admin', AdminUserModel::defaultRole()); 92 | } 93 | 94 | public function testMagicStatic() 95 | { 96 | double::registerClass('\demo\UserModel', ['defaultRole' => 'admin']); 97 | $this->assertSame('admin', UserModel::defaultRole()); 98 | } 99 | 100 | // public function testStubFunctionCall() 101 | // { 102 | // double::registerFunc('file_put_contents', 'Done'); 103 | // $user = new UserModel(); 104 | // $user->setName('David Bovie'); 105 | // $this->assertSame('Done', $user->dump()); 106 | // } 107 | } 108 | -------------------------------------------------------------------------------- /tests/unit/VerifierTest.php: -------------------------------------------------------------------------------- 1 | 'foo', 27 | 'email' => 'foo@bar.cl', 28 | ); 29 | 30 | $user = new UserModel(); 31 | double::registerObject($user); 32 | $user = new InstanceProxy($user); 33 | $user->setInfo($info); 34 | $user->setInfo([]); 35 | 36 | $matcher = function($params) use ($info) { 37 | $args = $params[0][0]; // first call, first arg 38 | $empty = $params[1][0]; // second call, first arg 39 | 40 | verify($info)->equals($args); 41 | verify($empty)->empty(); 42 | }; 43 | 44 | $this->specify('closure was called', function() use ($user, $info, $matcher) { 45 | $user->verifyInvokedMultipleTimes('setInfo', 2, $matcher); 46 | $user->verifyInvoked('setInfo', $matcher); 47 | }); 48 | } 49 | 50 | public function testVerifyMagicMethods() 51 | { 52 | $this->specify('works for class proxy', function() { 53 | // Set up user object. 54 | double::registerClass("demo\UserModel", 55 | ['renameUser'=>"Bob Jones", 'save'=>null]); 56 | $userProxy = new ClassProxy("demo\UserModel"); 57 | $user = new UserModel(['name'=>"John Smith"]); 58 | 59 | // Rename the user via magic method. 60 | UserService::renameStatic($user, "Bob Jones"); 61 | 62 | // Assert rename was counted. 63 | $userProxy->verifyInvoked('renameUser'); 64 | }); 65 | 66 | $this->specify('works for instance proxy', function() { 67 | // Set up user object. 68 | $user = new UserModel(['name'=>"John Smith"]); 69 | double::registerObject($user); 70 | $user = new InstanceProxy($user); 71 | 72 | // Rename the user via magic method. 73 | $user->renameUser("Bob Jones"); 74 | 75 | // Assert rename was counted. 76 | $user->verifyInvoked('renameUser'); 77 | }); 78 | } 79 | 80 | public function testverifyWithMutliplesParams() 81 | { 82 | $this->specify('works for instance proxy', function () { 83 | // Set up user object. 84 | $user = new UserModel(['name' => "John Smith"]); 85 | double::registerObject($user); 86 | $user = new InstanceProxy($user); 87 | 88 | // Rename the user 89 | $user->setName("Bob Jones"); 90 | 91 | // Assert rename was counted. 92 | $user->verifyInvoked('setName', "Bob Jones"); 93 | // if verifyInvoked is ok, verifyNeverInvoked have to fail 94 | try { 95 | $user->verifyNeverInvoked('setName', "Bob Jones"); 96 | // If i dont fail, my test fail 97 | throw new fail('verifyNeverInvoked'); 98 | } catch (Exception $e) {} 99 | 100 | $user->verifyNeverInvoked('setName', ["Boby Jones"]); 101 | 102 | // call function with multiple params 103 | $user->setNameAndInfo("Bob Jones", "Infos"); 104 | 105 | // verify 106 | $user->verifyInvoked('setNameAndInfo', ["Bob Jones", "Infos"]); 107 | 108 | // if verifyInvoked is ok, verifyNeverInvoked have to fail 109 | try { 110 | $user->verifyNeverInvoked('setNameAndInfo', ["Bob Jones", "Infos"]); 111 | // If i dont fail, my test fail 112 | throw new fail('verifyNeverInvoked'); 113 | } catch (Exception $e) { 114 | 115 | } 116 | }); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/unit/testDoubleTest.php: -------------------------------------------------------------------------------- 1 | null]); 24 | (new demo\UserModel())->save(); 25 | $user->verifyInvoked('save'); 26 | UserModel::tableName(); 27 | UserModel::tableName(); 28 | $user->verifyInvokedMultipleTimes('tableName',2); 29 | 30 | $this->specify('disabling all methods', function() use ($user) { 31 | test::methods($user, []); 32 | verify(UserModel::tableName())->null(); 33 | }); 34 | } 35 | 36 | public function testDoubleFullyQualifiedClass() 37 | { 38 | $user = test::double('\demo\UserModel', ['save' => null]); 39 | (new demo\UserModel())->save(); 40 | $user->verifyInvoked('save'); 41 | UserModel::tableName(); 42 | UserModel::tableName(); 43 | $user->verifyInvokedMultipleTimes('tableName',2); 44 | 45 | $this->specify('disabling all methods', function() use ($user) { 46 | test::methods($user, []); 47 | verify(UserModel::tableName())->null(); 48 | }); 49 | } 50 | 51 | public function testDoubleObject() 52 | { 53 | $user = new demo\UserModel(); 54 | $user = test::double($user, ['save' => null]); 55 | $user->save(); 56 | $user->verifyInvoked('save'); 57 | 58 | $this->specify('only selected methods can be added to instance', function() use ($user) { 59 | $user = test::methods($user, ['setName']); 60 | $user->setName('davert'); 61 | verify($user->getName())->notEquals('davert'); 62 | verify($user->getName())->null(); 63 | verify($user->getObject()->getName())->null(); 64 | }); 65 | } 66 | 67 | public function testSpecUndefinedClass() 68 | { 69 | $class = test::spec('MyVirtualClass'); 70 | /** @var $class ClassProxy **/ 71 | $this->assertFalse($class->isDefined()); 72 | $this->assertFalse($class->hasMethod('__toString')); 73 | $this->assertFalse($class->hasMethod('edit')); 74 | verify($class->interfaces())->empty(); 75 | $this->any = $class->make(); 76 | $this->any = $class->construct(); 77 | 78 | $this->specify('should return original class name', function() { 79 | $this->assertStringContainsString('Undefined', (string)$this->any); 80 | $this->assertStringContainsString('MyVirtualClass', (string)$this->any->__toString()); 81 | }); 82 | 83 | $this->specify('any method can be invoked', function() { 84 | $this->assertInstanceOf('AspectMock\Proxy\Anything', $this->any->doSmth()->withTHis()->andThatsAll()->null()); 85 | }); 86 | 87 | $this->specify('any property can be accessed', function() { 88 | $this->any->that = 'xxx'; 89 | $this->assertInstanceOf('AspectMock\Proxy\Anything', $this->any->this->that->another); 90 | }); 91 | 92 | $this->specify('can be used as array', function() { 93 | $this->any['has keys']; 94 | unset($this->any['this']); 95 | $this->any['this'] = 'that'; 96 | $this->assertFalse(isset($this->any['that'])); 97 | $this->assertInstanceOf('AspectMock\Proxy\Anything', $this->any['keys']); 98 | }); 99 | 100 | $this->specify('can be iterated', function() { 101 | foreach ($this->any as $anything) {} 102 | }); 103 | 104 | $this->specify('proxifies magic method calls', function() { 105 | $any = test::double($this->any); 106 | $any->callMeMaybe(); 107 | $any->name = 'hello world'; 108 | $this->assertInstanceOf('AspectMock\Proxy\Anything', $any->name); 109 | verify($any->class->className)->equals('AspectMock\Proxy\Anything'); 110 | }); 111 | } 112 | 113 | public function testCleanupSpecificClasses() 114 | { 115 | $service = test::double('demo\UserService',['updateName' => 'hello'])->make(); 116 | test::double('demo\UserModel',['tableName' => 'my_table']); 117 | verify(demo\UserModel::tableName())->equals('my_table'); 118 | test::clean('demo\UserModel'); 119 | verify(demo\UserModel::tableName())->equals('users'); 120 | verify($service->updateName(new UserModel()))->equals('hello'); 121 | } 122 | 123 | public function testCleanupSpecificObj() 124 | { 125 | $model = test::double('demo\UserModel'); 126 | $user1 = test::double($model->make(), ['getName' => 'bad boy']); 127 | $user2 = test::double($model->make(), ['getName' => 'good boy']); 128 | verify($user1->getName())->equals('bad boy'); 129 | verify($user2->getName())->equals('good boy'); 130 | test::clean($user1); 131 | verify($user1->getName())->null(); 132 | verify($user2->getName())->equals('good boy'); 133 | } 134 | 135 | public function testPhp7Features() 136 | { 137 | Kernel::getInstance()->loadFile(codecept_data_dir() . 'php7.php'); 138 | test::double(TestPhp7Class::class, [ 139 | 'stringSth' => true, 140 | 'floatSth' => true, 141 | 'boolSth' => true, 142 | 'intSth' => true, 143 | 'callableSth' => true, 144 | 'arraySth' => true, 145 | 'variadicStringSthByRef' => true, 146 | 'stringRth' => 'hey', 147 | 'floatRth' => 12.2, 148 | 'boolRth' => true, 149 | 'intRth' => 12, 150 | 'callableRth' => function() { return function() {}; }, 151 | 'arrayRth' => [1], 152 | 'exceptionRth' => new Exception(), 153 | ]); 154 | $obj = new TestPhp7Class; 155 | $this->assertTrue($obj->stringSth('123')); 156 | $this->assertTrue($obj->floatSth(123)); 157 | $this->assertTrue($obj->boolSth(false)); 158 | $this->assertTrue($obj->intSth(12)); 159 | $this->assertTrue($obj->callableSth(function() {})); 160 | $this->assertTrue($obj->arraySth([])); 161 | $str = 'hello'; 162 | $this->assertTrue($obj->variadicStringSthByRef($str, $str)); 163 | $this->assertSame('hey', $obj->stringRth($str)); 164 | $this->assertSame(12.2, $obj->floatRth(12.12)); 165 | $this->assertTrue($obj->boolRth(false)); 166 | $this->assertSame(12, $obj->intRth(15)); 167 | //$this->assertInternalType('callable', $obj->callableRth(function() {})); 168 | $this->assertSame([1], $obj->arrayRth([])); 169 | $this->assertInstanceOf('Exception', $obj->exceptionRth(new Exception('ups'))); 170 | } 171 | } 172 | --------------------------------------------------------------------------------