├── .gitignore ├── phpunit.xml ├── test ├── bootstrap.php └── Shmock │ ├── ClassBuilder │ ├── ClosureInspectorTest.php │ ├── MethodInspectorTest.php │ ├── DecoratorTest.php │ └── ClassBuilderTest.php │ ├── Shmockers_Test.php │ ├── MockChecker.php │ ├── StaticSpecTest.php │ ├── Policy_Test.php │ ├── ShmockTest.php │ └── StaticMockTest.php ├── src └── Shmock │ ├── Constraints │ ├── AnyTimes.php │ ├── Frequency.php │ ├── AtLeastOnce.php │ ├── Ordering.php │ ├── CountOfTimes.php │ ├── Unordered.php │ └── MethodNameOrdering.php │ ├── ClassBuilder │ ├── CallableDecorator.php │ ├── Decorator.php │ ├── JoinPoint.php │ ├── ClosureInspector.php │ ├── Invocation.php │ ├── MethodInspector.php │ ├── DecoratorJoinPoint.php │ └── ClassBuilder.php │ ├── InstanceSpec.php │ ├── Shmockers.php │ ├── Instance.php │ ├── Policy.php │ ├── Shmock.php │ ├── Spec.php │ ├── ClassBuilderStaticClass.php │ ├── ClassBuilderInstanceClass.php │ └── StaticSpec.php ├── .php_cs ├── composer.json ├── CONTRIBUTING.md ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | pages 2 | vendor 3 | *.iml 4 | .idea 5 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/bootstrap.php: -------------------------------------------------------------------------------- 1 | notName('README.md') 7 | ->notName('.php_cs') 8 | ->notName('composer.*') 9 | ->notName('phpunit.xml*') 10 | ->notName('*.phar') 11 | ->exclude('vendor') 12 | ->in(__DIR__) 13 | ; 14 | 15 | return Symfony\CS\Config\Config::create() 16 | ->finder($finder) 17 | ; 18 | -------------------------------------------------------------------------------- /src/Shmock/ClassBuilder/CallableDecorator.php: -------------------------------------------------------------------------------- 1 | fn = $fn; 21 | } 22 | 23 | /** 24 | * @param JoinPoint $joinPoint 25 | * @return mixed|null 26 | */ 27 | public function decorate(JoinPoint $joinPoint) 28 | { 29 | return call_user_func($this->fn, $joinPoint); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Shmock/Constraints/Frequency.php: -------------------------------------------------------------------------------- 1 | execute()` directly. 19 | */ 20 | public function decorate(JoinPoint $joinPoint); 21 | } 22 | -------------------------------------------------------------------------------- /src/Shmock/InstanceSpec.php: -------------------------------------------------------------------------------- 1 | arguments) > 0 || Shmock::$check_args_for_policy_on_instance_method_when_no_args_passed; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /test/Shmock/ClassBuilder/ClosureInspectorTest.php: -------------------------------------------------------------------------------- 1 | assertSame($inspector->signatureArgs(), $typeHints); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "box/shmock", 3 | "type": "library", 4 | "description": "Shorthand for (PHPUnit) Mocking", 5 | "keywords": ["shmock","mock","phpunit"], 6 | "homepage": "http://github.com/box/shmock", 7 | "license": "Apache 2.0", 8 | "authors": [ 9 | { 10 | "name": "Anthony Bishopric", 11 | "email": "abishopric@box.com", 12 | "homepage": "https://github.com/anthonybishopric" 13 | } 14 | ], 15 | "require": { 16 | "php": ">=5.4.0", 17 | "phpunit/phpunit": ">=3.7", 18 | "nicmart/string-template": "0.1.0", 19 | "sebastian/diff": ">=1.1" 20 | }, 21 | "require-dev": { 22 | "phpdocumentor/phpdocumentor": "2.*", 23 | "ext-augmented_types": "*" 24 | }, 25 | "autoload": { 26 | "psr-0": {"Shmock": "src/"} 27 | }, 28 | "extra": { 29 | "branch-alias": { 30 | "dev-master": "2.0.x-dev" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Shmock/Constraints/AtLeastOnce.php: -------------------------------------------------------------------------------- 1 | methodName = $methodName; 23 | } 24 | 25 | /** 26 | * @return void 27 | */ 28 | public function addCall() 29 | { 30 | $this->called = true; 31 | } 32 | 33 | /** 34 | * @return void 35 | */ 36 | public function verify() 37 | { 38 | if (!$this->called) { 39 | throw new \PHPUnit_Framework_AssertionFailedError(sprintf("Expected %s to be called at least once", $this->methodName)); 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/Shmock/Shmockers.php: -------------------------------------------------------------------------------- 1 | shmock('Shmock\Email_Service', function ($user) { 15 | $user->send('user@gmail.com', $this->stringContains("a message")); 16 | }); 17 | 18 | $user = new User($shmock_email_service); 19 | $user->set_email_address('user@gmail.com'); 20 | $user->send_email("This is a message"); 21 | } 22 | } 23 | 24 | class User 25 | { 26 | private $email_address; 27 | private $email_service; 28 | 29 | public function __construct($email_service) 30 | { 31 | $this->email_service = $email_service; 32 | } 33 | 34 | public function set_email_address($email) 35 | { 36 | $this->email_address = $email; 37 | } 38 | 39 | public function send_email($message) 40 | { 41 | $this->email_service->send($this->email_address, $message); 42 | } 43 | } 44 | 45 | class Email_Service 46 | { 47 | public function send($email, $message) 48 | { 49 | // no-op, just for testing 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Shmock/ClassBuilder/JoinPoint.php: -------------------------------------------------------------------------------- 1 | execute()` may not directly 9 | * trigger the underlying method, but instead invoke the next Decorator 10 | * in the chain. 11 | */ 12 | interface JoinPoint 13 | { 14 | /** 15 | * @return string|object the target object or class that is the receiver 16 | * of this invocation. If this method is static, the target is the name 17 | * of the class. 18 | */ 19 | public function target(); 20 | 21 | /** 22 | * @return string The name of the method that is being invoked. 23 | */ 24 | public function methodName(); 25 | 26 | /** 27 | * @return array the list of arguments that is currently slated to be sent 28 | * to the underlying function 29 | */ 30 | public function arguments(); 31 | 32 | /** 33 | * Alter the arguments to be sent to the underlying method 34 | * @param array $newArguments 35 | * @return void 36 | */ 37 | public function setArguments(array $newArguments); 38 | 39 | /** 40 | * @return mixed|null invokes the underlying method or another JoinPoint handler. 41 | */ 42 | public function execute(); 43 | } 44 | -------------------------------------------------------------------------------- /src/Shmock/Constraints/Ordering.php: -------------------------------------------------------------------------------- 1 | expectedCount = $count; 23 | $this->methodName = $methodName; 24 | } 25 | 26 | /** 27 | * @return void 28 | */ 29 | public function addCall() 30 | { 31 | $this->actualCount++; 32 | if ($this->actualCount > $this->expectedCount) { 33 | throw new \PHPUnit_Framework_AssertionFailedError(sprintf("Didn't expect %s to be called more than %s times", $this->methodName, $this->expectedCount)); 34 | } 35 | } 36 | 37 | /** 38 | * @return void 39 | */ 40 | public function verify() 41 | { 42 | if ($this->actualCount != $this->expectedCount) { 43 | throw new \PHPUnit_Framework_AssertionFailedError(sprintf("Expected %s to be called exactly %s times, called %s times", $this->methodName, $this->expectedCount, $this->actualCount)); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/Shmock/ClassBuilder/MethodInspectorTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expectedSig, $inspector->signatureArgs()); 26 | 27 | } 28 | 29 | public function methodOneActingClinic($tobias, $lindsay) 30 | { 31 | } 32 | 33 | public function digitalWitness($annie = 1, $clark = 2.0) 34 | { 35 | } 36 | 37 | public function glassHandedKites($special = "hey", $apocalypso = null) 38 | { 39 | } 40 | 41 | public function halcyonDigest(array $thing, MethodInspectorTest $test = null) 42 | { 43 | } 44 | 45 | public function yanquiUXO(array &$thing) 46 | { 47 | 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Shmock/ClassBuilder/ClosureInspector.php: -------------------------------------------------------------------------------- 1 | func = $func; 18 | } 19 | 20 | /** 21 | * @return string[] the string representations of the type hints 22 | */ 23 | public function signatureArgs() 24 | { 25 | $reflMethod = new \ReflectionFunction($this->func); 26 | $parameters = $reflMethod->getParameters(); 27 | $result = []; 28 | foreach ($parameters as $parameter) { 29 | $arg = []; 30 | if ($parameter->isArray()) { 31 | $arg []= "array"; 32 | } elseif ($parameter->isCallable()) { 33 | $arg []= "callable"; 34 | } elseif ($parameter->getClass()) { 35 | $arg []= "\\". $parameter->getClass()->getName(); 36 | } 37 | if ($parameter->isPassedByReference()) { 38 | $arg[] = "&"; 39 | } 40 | $arg[]= "$" . $parameter->getName(); 41 | if ($parameter->isDefaultValueAvailable()) { 42 | $arg[] = "= " . var_export($parameter->getDefaultValue(), true); 43 | } 44 | $result[] = implode($arg, " "); 45 | } 46 | 47 | return $result; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/Shmock/MockChecker.php: -------------------------------------------------------------------------------- 1 | getParentClass(); 16 | $prop = $refl->getProperty('mockObjects'); 17 | $prop->setAccessible(true); 18 | $prop->setValue($this, array()); 19 | } 20 | 21 | protected function assertFailsMockExpectations(callable $fn, $message) 22 | { 23 | $threw = true; 24 | try { 25 | $fn(); 26 | $threw = false; 27 | } catch (\PHPUnit_Framework_AssertionFailedError $e) { 28 | 29 | } 30 | $this->assertTrue($threw, "Expected callable to throw phpunit failure: $message"); 31 | 32 | $this->resetMockObjects(); 33 | $this->staticClass = null; 34 | 35 | } 36 | 37 | protected function assertMockObjectsShouldFail($message) 38 | { 39 | $threw = true; 40 | try { 41 | $this->verifyMockObjects(); 42 | $this->staticClass->verify(); 43 | $threw = false; 44 | } catch ( \PHPUnit_Framework_AssertionFailedError $e) { 45 | 46 | } 47 | if (!$threw) { 48 | $this->fail("Expected mock objects to fail in PHPUnit: $message"); 49 | } 50 | $this->resetMockObjects(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Shmock/Instance.php: -------------------------------------------------------------------------------- 1 | target = $target; 28 | $this->methodName = $methodName; 29 | $this->arguments = $arguments; 30 | $this->parameters = $arguments; 31 | 32 | if (is_object($target)) { // if this is an instance method invocation 33 | $this->object = $target; 34 | $this->className = get_class($target); 35 | } else { 36 | $this->object = null; 37 | $this->className = $target; 38 | } 39 | } 40 | 41 | /** 42 | * @return string|object 43 | */ 44 | public function getTarget() 45 | { 46 | return $this->target; 47 | } 48 | 49 | /** 50 | * @return array 51 | */ 52 | public function getArguments() 53 | { 54 | return $this->arguments; 55 | } 56 | 57 | /** 58 | * @return string 59 | */ 60 | public function getMethodName() 61 | { 62 | return $this->methodName; 63 | } 64 | 65 | /** 66 | * @param callable 67 | * @return mixed|null 68 | */ 69 | public function callWith(callable $fn) 70 | { 71 | return call_user_func_array($fn, $this->arguments); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Shmock/Policy.php: -------------------------------------------------------------------------------- 1 | map[$methodName] = $spec; 29 | $this->orderedMap[] = [$methodName, $spec]; 30 | } 31 | 32 | /** 33 | * @param string 34 | * @return \Shmock\Spec 35 | */ 36 | public function nextSpec($methodName) 37 | { 38 | if (!array_key_exists($methodName, $this->map)) { 39 | // this assertion should never be reached if the class builder is properly 40 | // configured. 41 | throw new \LogicException("Did not expect invocation of $methodName"); 42 | } 43 | 44 | return $this->map[$methodName]; 45 | } 46 | 47 | /** 48 | * @return void 49 | */ 50 | public function reset() 51 | { 52 | // no-op 53 | } 54 | 55 | /** 56 | * @return void 57 | */ 58 | public function verify() 59 | { 60 | foreach ($this->map as $name => $expectation) { 61 | $expectation->__shmock_verify(); 62 | } 63 | } 64 | 65 | /** 66 | * @return MethodNameOrdering 67 | */ 68 | public function convertToMethodNameOrdering() 69 | { 70 | $methodOrdering = new MethodNameOrdering(); 71 | for ($i = 0; $i < count($this->orderedMap); $i++) { 72 | $methodOrdering->addSpec($this->orderedMap[$i][0], $this->orderedMap[$i][1]); 73 | } 74 | return $methodOrdering; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Shmock/ClassBuilder/MethodInspector.php: -------------------------------------------------------------------------------- 1 | className = $className; 27 | $this->methodName = $methodName; 28 | try { 29 | $this->reflMethod = new \ReflectionMethod($this->className, $this->methodName); 30 | } catch (\ReflectionException $e) { 31 | $this->reflMethod = null; 32 | } 33 | } 34 | 35 | /** 36 | * @return string[] the string representations of function's arguments 37 | */ 38 | public function signatureArgs() 39 | { 40 | if ($this->reflMethod == null) { 41 | return [""]; 42 | } 43 | $parameters = $this->reflMethod->getParameters(); 44 | $result = []; 45 | foreach ($parameters as $parameter) { 46 | $arg = []; 47 | if ($parameter->isArray()) { 48 | $arg []= "array"; 49 | } elseif ($parameter->isCallable()) { 50 | $arg []= "callable"; 51 | } elseif ($parameter->getClass()) { 52 | $arg []= "\\". $parameter->getClass()->getName(); 53 | } 54 | if ($parameter->isPassedByReference()) { 55 | $arg[] = "&"; 56 | } 57 | $arg[]= "$" . $parameter->getName(); 58 | if ($parameter->isDefaultValueAvailable()) { 59 | $arg[] = "= " . var_export($parameter->getDefaultValue(), true); 60 | } 61 | $result[] = implode($arg, " "); 62 | } 63 | 64 | return $result; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /test/Shmock/ClassBuilder/DecoratorTest.php: -------------------------------------------------------------------------------- 1 | setDecorators([new CalculatorDecorator()]); 12 | $joinPoint->setArguments([2, 2]); 13 | $val = $joinPoint->execute(); 14 | $this->assertEquals(12, $val); 15 | 16 | } 17 | 18 | public function testDecoratorShouldWrapStaticMethodReceivers() 19 | { 20 | $joinPoint = new DecoratorJoinPoint('\Shmock\ClassBuilder\Calculator', 'subtract'); 21 | $joinPoint->setDecorators([new CalculatorDecorator()]); 22 | $joinPoint->setArguments([10,3]); 23 | $val = $joinPoint->execute(); 24 | $this->assertEquals(12, $val); 25 | } 26 | 27 | public function testJoinPointHandlesArbitraryNumberOfDecorators() 28 | { 29 | $calculator = new Calculator(); 30 | $joinPoint = new DecoratorJoinPoint($calculator, "multiply"); 31 | $joinPoint->setDecorators([new CalculatorDecorator(), new CalculatorDecorator(), new CalculatorDecorator()]); 32 | $joinPoint->setArguments([2, 2]); 33 | $val = $joinPoint->execute(); 34 | $this->assertEquals(80, $val); 35 | 36 | } 37 | 38 | public function testAlternateCallableCanBeSpecified() 39 | { 40 | $joinPoint = new DecoratorJoinPoint(new Calculator(), "multiply", function (Invocation $invocation) { 41 | list($a, $b) = $invocation->getArguments(); 42 | 43 | return $a * $b * $b; 44 | }); 45 | $joinPoint->setDecorators([new CalculatorDecorator()]); 46 | $joinPoint->setArguments([2,3]); 47 | $val = $joinPoint->execute(); 48 | $this->assertEquals(64, $val); 49 | } 50 | 51 | /** 52 | * @expectedException \InvalidArgumentException 53 | */ 54 | public function testInvalidMethodsWillResultInInvalidArgumentException() 55 | { 56 | new DecoratorJoinPoint(new CalculatorDecorator(), "doubleIt"); 57 | } 58 | 59 | } 60 | 61 | class CalculatorDecorator implements Decorator 62 | { 63 | public function decorate(JoinPoint $joinPoint) 64 | { 65 | $args = $joinPoint->arguments(); 66 | $args[1]++; 67 | $joinPoint->setArguments($args); 68 | 69 | return 2 * $joinPoint->execute(); 70 | } 71 | } 72 | 73 | class Calculator 74 | { 75 | public function multiply(Invocation $inv) 76 | { 77 | list($x, $y) = $inv->getArguments(); 78 | 79 | return $x * $y; 80 | } 81 | 82 | public static function subtract(Invocation $inv) 83 | { 84 | list($x, $y) = $inv->getArguments(); 85 | 86 | return $x - $y; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/Shmock/Constraints/MethodNameOrdering.php: -------------------------------------------------------------------------------- 1 | chain[] = [$methodName, $spec]; 26 | } 27 | 28 | /** 29 | * This naive implementation of ordering will ignore 30 | * frequency requirements. 31 | * @param string $methodName 32 | * @return \Shmock\Spec 33 | */ 34 | public function nextSpec($methodName) 35 | { 36 | if ($this->pointer >= count($this->chain)) { 37 | throw new \PHPUnit_Framework_AssertionFailedError("There are no more method calls expected given assigned ordering"); 38 | } 39 | if ($this->pointer + 1 < count($this->chain)) { 40 | $next = $this->chain[$this->pointer + 1]; 41 | if ($next[0] === $methodName) { 42 | // if the next method has the same name as the passed 43 | // $methodName, then advance the pointer and return the 44 | // given spec. 45 | $this->pointer++; 46 | 47 | return $next[1]; 48 | } 49 | } 50 | 51 | if ($this->pointer === -1) { 52 | throw new \PHPUnit_Framework_AssertionFailedError(sprintf("Unexpected method invocation %s at call index %s, haven't seen any method calls yet", $methodName, $this->pointer, true)); 53 | } 54 | $current = $this->chain[$this->pointer]; 55 | if ($current[0] !== $methodName) { 56 | // if the current spec does not match the method name, 57 | // we do not have any other possible specs to return 58 | // so we throw an assertion failure with a print of every 59 | // method we've run so far: 60 | $methodsSoFar = []; 61 | for ($i = 0; $i <= $this->pointer; $i++) { 62 | $methodsSoFar[] = $this->chain[$i][0]; 63 | } 64 | 65 | throw new \PHPUnit_Framework_AssertionFailedError(sprintf("Unexpected method invocation %s at call index %s, seen method calls so far: %s", $methodName, $this->pointer, print_r(implode($methodsSoFar, "\n"), true))); 66 | } 67 | 68 | return $current[1]; 69 | } 70 | 71 | /** 72 | * Reset the ordering back to its initial state. 73 | * @return void 74 | */ 75 | public function reset() 76 | { 77 | $this->pointer = 0; 78 | } 79 | 80 | /** 81 | * @return void 82 | */ 83 | public function verify() 84 | { 85 | foreach ($this->chain as $specWithName) { 86 | $specWithName[1]->__shmock_verify(); 87 | } 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | All contributions are welcome to this project. 4 | 5 | ## Contributor License Agreement 6 | 7 | Before a contribution can be merged into this project, please fill out the Contributor License Agreement (CLA) located at: 8 | 9 | http://box.github.io/cla 10 | 11 | To learn more about CLAs and why they are important to open source projects, please see the [Wikipedia entry](http://en.wikipedia.org/wiki/Contributor_License_Agreement). 12 | 13 | ## How to contribute 14 | 15 | * **File an issue** - if you found a bug, want to request an enhancement, or want to implement something (bug fix or feature). 16 | * **Send a pull request** - if you want to contribute code. Please be sure to file an issue first. 17 | 18 | ## Pull request best practices 19 | 20 | We want to accept your pull requests. Please follow these steps: 21 | 22 | ### Step 1: File an issue 23 | 24 | Before writing any code, please file an issue stating the problem you want to solve or the feature you want to implement. This allows us to give you feedback before you spend any time writing code. There may be a known limitation that can't be addressed, or a bug that has already been fixed in a different way. The issue allows us to communicate and figure out if it's worth your time to write a bunch of code for the project. 25 | 26 | ### Step 2: Fork this repository in GitHub 27 | 28 | This will create your own copy of our repository. 29 | 30 | ### Step 3: Add the upstream source 31 | 32 | The upstream source is the project under the Box organization on GitHub. To add an upstream source for this project, type: 33 | 34 | ``` 35 | git remote add upstream git@github.com:Box/shmock.git 36 | ``` 37 | 38 | This will come in useful later. 39 | 40 | ### Step 4: Create a feature branch 41 | 42 | Create a branch with a descriptive name, such as `add-search`. 43 | 44 | ### Step 5: Install and use our preferred pre-commit hook (or you can configure your environment to perform the equivalent as needed) 45 | 46 | ```bash 47 | set -e 48 | vendor/bin/phpunit test/ 49 | vendor/bin/phpdoc.php run -d src/ -t pages 50 | git diff --cached --name-only --diff-filter=AMRC | grep ".php" | xargs -n 1 -I % sh -c "echo 'formatting %'; php-cs-fixer fix %; git add %" 51 | exit 0 52 | ``` 53 | The above will: 54 | 1. ensure that all PHPUnit tests pass 55 | 2. generate up-to-date documentation in a gitignored directory called "pages", which should be pushed to the gh-pages branch. 56 | 3. execute php-cs-fixer on every altered non-deleted php file and then git add the change 57 | 58 | ### Step 6: Push your feature branch to your fork 59 | 60 | As you develop code, continue to push code to your remote feature branch. Please make sure to include the issue number you're addressing in your commit message, such as: 61 | 62 | ``` 63 | git commit -m "Adding search (fixes #123)" 64 | ``` 65 | 66 | This helps us out by allowing us to track which issue your commit relates to. 67 | 68 | Keep a separate feature branch for each issue you want to address. 69 | 70 | ### Step 7: Rebase 71 | 72 | Before sending a pull request, rebase against upstream, such as: 73 | 74 | ``` 75 | git fetch upstream 76 | git rebase upstream/master 77 | ``` 78 | 79 | This will add your changes on top of what's already in upstream, minimizing merge issues. 80 | 81 | ### Step 8: Send the pull request 82 | 83 | Send the pull request from your feature branch to us. Be sure to include a description that lets us know what work you did. 84 | 85 | Keep in mind that we like to see one issue addressed per pull request, as this helps keep our git history clean and we can more easily track down issues. 86 | -------------------------------------------------------------------------------- /src/Shmock/ClassBuilder/DecoratorJoinPoint.php: -------------------------------------------------------------------------------- 1 | decorate($this)`. It will do this until no remaining 12 | * `Decorator` instances exist in the array, at which point it will invoke the 13 | * underlying method. The counter is decrement after each execution, allowing 14 | * for multiple invocations. 15 | */ 16 | class DecoratorJoinPoint implements JoinPoint 17 | { 18 | /** 19 | * @internal 20 | * @var Decorator[] 21 | */ 22 | private $decorators = []; 23 | 24 | /** 25 | * @internal 26 | * @var int 27 | */ 28 | private $index = 0; 29 | 30 | /** 31 | * @internal 32 | * @var string|object 33 | */ 34 | private $target; 35 | 36 | /** 37 | * @internal 38 | * @var string 39 | */ 40 | private $methodName; 41 | 42 | /** 43 | * @internal 44 | * @var callable 45 | */ 46 | private $actualCallable; 47 | 48 | /** 49 | * @internal 50 | * @var array 51 | */ 52 | private $arguments; 53 | 54 | /** 55 | * @return mixed|null 56 | */ 57 | public function execute() 58 | { 59 | if ($this->index >= count($this->decorators)) { 60 | $invocation = new Invocation($this->target, $this->methodName, $this->arguments); 61 | 62 | return call_user_func($this->actualCallable, $invocation); 63 | } else { 64 | $nextDecorator = $this->decorators[$this->index]; 65 | $this->index++; 66 | $ret = $nextDecorator->decorate($this); 67 | $this->index--; 68 | 69 | return $ret; 70 | } 71 | } 72 | 73 | /** 74 | * @param string|object $target The method receiver, which may be a class or 75 | * instance. 76 | * @param string $methodName the method to invoke 77 | * @param callable|void $callable specifically for the given 78 | * target and methodName. This is useful when you wish a Decorator 79 | * to decorate according to the target/method but need another layer 80 | * of indirection (via a proxy potentially). If not specified, it 81 | * will be composed from [$target, $callable] 82 | */ 83 | public function __construct($target, $methodName, $callable=null) 84 | { 85 | $this->target = $target; 86 | $this->methodName = $methodName; 87 | $this->actualCallable = $callable ?: [$target, $methodName]; 88 | 89 | if (!method_exists($target, $methodName)) { 90 | throw new \InvalidArgumentException("$methodName is not a method on the given target"); 91 | } 92 | } 93 | 94 | /** 95 | * @param array $newArguments the new arguments to pass 96 | * @return void 97 | */ 98 | public function setArguments(array $newArgs) 99 | { 100 | $this->arguments = $newArgs; 101 | } 102 | 103 | /** 104 | * @param Decorator[] Decorators that will wrap this execution 105 | * @return void 106 | */ 107 | public function setDecorators(array $decorators) 108 | { 109 | $this->decorators = $decorators; 110 | } 111 | 112 | /** 113 | * @return string|object 114 | */ 115 | public function target() 116 | { 117 | return $this->target; 118 | } 119 | 120 | /** 121 | * @return string 122 | */ 123 | public function methodName() 124 | { 125 | return $this->methodName; 126 | } 127 | 128 | /** 129 | * @return array 130 | */ 131 | public function arguments() 132 | { 133 | return $this->arguments; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /test/Shmock/StaticSpecTest.php: -------------------------------------------------------------------------------- 1 | setExtends("\Shmock\Policy"); 18 | 19 | $builder->addMethod($method, function (Invocation $i) use ($fn) { 20 | return $i->callWith($fn); 21 | }, ['$className', '$methodName', '$value', '$static']); 22 | 23 | $clazz = $builder->create(); 24 | 25 | return new $clazz(); 26 | } 27 | 28 | private function assertThrowsShmockException(callable $fn, $message) 29 | { 30 | try { 31 | $fn(); 32 | $this->fail($message); 33 | } catch (FakeShmockException $e) { 34 | $this->assertTrue(true); 35 | } 36 | } 37 | 38 | public function testSpecWillVerifyArgumentsWithPolicies() 39 | { 40 | $policy = $this->buildPolicy('check_method_parameters', function ($className, $methodName, array $parameters, $static) { 41 | $this->assertSame(get_class($this), $className); 42 | $this->assertSame('aStaticMethod', $methodName); 43 | $this->assertTrue($static); 44 | 45 | if ([2,3,4] !== $parameters) { 46 | throw new FakeShmockException(); 47 | } 48 | }); 49 | 50 | $spec = new StaticSpec($this, get_class($this), 'aStaticMethod', [2,3,4], [$policy]); 51 | 52 | $this->assertThrowsShmockException(function () use ($policy) { 53 | $spec = new StaticSpec($this, get_class($this), 'aStaticMethod', [2,4,4], [$policy]); 54 | }, 'Should have failed to instantiate a spec with invalid arguments'); 55 | } 56 | 57 | public function testSpecWillVerifyReturnValuesWithPolicies() 58 | { 59 | $policy = $this->buildPolicy('check_method_return_value', function ($className, $methodName, $retVal, $static) { 60 | $this->assertSame(get_class($this), $className); 61 | $this->assertSame('aStaticMethod', $methodName); 62 | $this->assertTrue($static); 63 | 64 | if ($retVal !== 10) { 65 | throw new FakeShmockException(); 66 | } 67 | }); 68 | 69 | $spec = new StaticSpec($this, get_class($this), 'aStaticMethod', [], [$policy]); 70 | $spec->return_value(10); 71 | 72 | $this->assertThrowsShmockException(function () use ($spec) { 73 | $spec->return_value(11); 74 | }, "Should have erred when mocking with an inappropriate value"); 75 | } 76 | 77 | public function testSpecWillVerifyThrowsMatch() 78 | { 79 | $policy = $this->buildPolicy('check_method_throws', function ($className, $methodName, $exception, $static) { 80 | $this->assertSame(get_class($this), $className); 81 | $this->assertSame('aStaticMethod', $methodName); 82 | $this->assertTrue($static); 83 | 84 | if (get_class($exception) === 'Exception') { 85 | throw new FakeShmockException(); 86 | } 87 | }); 88 | 89 | $spec = new StaticSpec($this, get_class($this), 'aStaticMethod', [], [$policy]); 90 | $spec->throw_exception(new \InvalidArgumentException()); 91 | 92 | $this->assertThrowsShmockException(function () use ($spec) { 93 | $spec->throw_exception(new \Exception()); 94 | }, "Should have erred when mocking with an inappropriate value"); 95 | 96 | } 97 | 98 | public function testReturnValueCorrectlyEvaluatesCorrectnessOfParameters() 99 | { 100 | $policy = $this->buildPolicy('check_method_parameters', function ($className, $methodName, $parameters, $statc) { 101 | if ($parameters != [] && (($parameters[0] + $parameters[1]) % 3 == 1)) { 102 | throw new FakeShmockException("Cannot be divisble by 3n+1"); 103 | } 104 | }); 105 | 106 | $spec = new StaticSpec($this, get_class($this), 'aStaticMethod', [], [$policy]); 107 | $spec->return_value_map([ 108 | [3,3,6], 109 | [2,4,6], 110 | [1,2,3], 111 | ]); 112 | 113 | $this->assertThrowsShmockException(function () use ($spec) { 114 | $spec->return_value_map([ 115 | [3,4,7] 116 | ]); 117 | }, "Expected policy to throw on given arguments"); 118 | } 119 | 120 | public static function aStaticMethod() 121 | { 122 | 123 | } 124 | } 125 | 126 | class FakeShmockException extends \Exception{} 127 | -------------------------------------------------------------------------------- /test/Shmock/ClassBuilder/ClassBuilderTest.php: -------------------------------------------------------------------------------- 1 | create(); 15 | } 16 | 17 | public function testClassesCanBeBuilt() 18 | { 19 | $this->assertTrue(class_exists($this->buildClass()), "The class should have been created"); 20 | } 21 | 22 | public function testClassesCanBeMarkedAsSubclassesOfOtherClasses() 23 | { 24 | $class = $this->buildClass(function ($builder) { 25 | $builder->setExtends(get_class($this)); 26 | }); 27 | $this->assertTrue(is_subclass_of($class, get_class($this))); 28 | } 29 | 30 | public function testClassesCanHaveFunctionsAttached() 31 | { 32 | $class = $this->buildClass(function ($builder) { 33 | $builder->addMethod("add", function (Invocation $invocation) { 34 | list($a, $b) = $invocation->getArguments(); 35 | 36 | return $a + $b; 37 | }, ['$a', '$b']); 38 | }); 39 | 40 | $this->assertEquals(2, (new $class)->add(1,1)); 41 | } 42 | 43 | public function testClassBuilderCanExtendOtherClasses() 44 | { 45 | $class = $this->buildClass(function ($builder) { 46 | $builder->addMethod("add", function (Invocation $invocation) { 47 | list($a, $b) = $invocation->getArguments(); 48 | 49 | return $a + $b; 50 | }, ['$a', '$b']); 51 | $builder->setExtends("Shmock\ClassBuilder\SampleExtension"); 52 | }); 53 | 54 | $instance = new $class(); 55 | $this->assertEquals(15, $instance->multiply($instance->add(1,2), 5)); 56 | } 57 | 58 | public function testClassesPassAlongTypeHints() 59 | { 60 | $class = $this->buildClass(function ($builder) { 61 | $builder->addInterface("Shmock\ClassBuilder\SampleInterface"); 62 | $builder->addMethod("firstAndLast", function (Invocation $invocation) { 63 | list($a, $b) = $invocation->getArguments(); 64 | 65 | return array_merge($a,$b); 66 | }, ['array $a', 'array $b']); 67 | }); 68 | $instance = new $class(); 69 | $this->assertEquals([1,-1], $instance->firstAndLast([1], [-1])); 70 | $this->assertTrue(is_subclass_of($class, 'Shmock\ClassBuilder\SampleInterface')); 71 | } 72 | 73 | private $counter = 0; 74 | 75 | public function testAllMockMethodsCanBeDecorated() 76 | { 77 | $class = $this->buildClass(function ($builder) { 78 | $builder->addMethod("multiply", function (Invocation $invocation) { 79 | list($a, $b) = $invocation->getArguments(); 80 | 81 | return $a * $b; 82 | }, ['$x', '$y']); 83 | $builder->addMethod("add", function (Invocation $invocation) { 84 | list($a, $b) = $invocation->getArguments(); 85 | 86 | return $a + $b; 87 | }, ['$a', '$b']); 88 | $builder->addDecorator(function (JoinPoint $joinPoint) { 89 | $this->counter++; 90 | 91 | return $joinPoint->execute(); 92 | }); 93 | }); 94 | $instance = new $class(); 95 | $this->assertEquals(6, $instance->add(3,3)); 96 | $this->assertEquals(9, $instance->multiply(3,3)); 97 | $this->assertEquals(2, $this->counter); 98 | } 99 | 100 | public function testMethodsCanBeSpecifiedAsStatic() 101 | { 102 | $class = $this->buildClass(function ($builder) { 103 | $builder->addStaticMethod("multiply", function (Invocation $invocation) { 104 | list($a, $b) = $invocation->getArguments(); 105 | 106 | return $a * $b; 107 | }, ['$a', '$b']); 108 | }); 109 | 110 | $this->assertEquals(10, $class::multiply(2, 5)); 111 | } 112 | 113 | public function testTraitsCanBeIncludedInNewClasses() 114 | { 115 | $class = $this->buildClass(function ($builder) { 116 | $builder->addTrait('Shmock\ClassBuilder\SampleTrait'); 117 | }); 118 | 119 | $instance = new $class(); 120 | $this->assertSame("#FF0000", $instance->redHex()); 121 | } 122 | 123 | } 124 | 125 | abstract class SampleExtension 126 | { 127 | public function multiply($x, $y) 128 | { 129 | return $x * $y; 130 | } 131 | } 132 | 133 | interface SampleInterface 134 | { 135 | public function firstAndLast(array $first, array $last); 136 | } 137 | 138 | trait SampleTrait 139 | { 140 | public function redHex() 141 | { 142 | return "#FF0000"; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /test/Shmock/Policy_Test.php: -------------------------------------------------------------------------------- 1 | shmock('Shmock\Even_Calculator', function ($calculator) { 31 | $calculator->raise_to_even(4.4)->return_value(6); 32 | }); 33 | 34 | $this->fail("should not reach here"); 35 | } 36 | 37 | public function test_policies_allow_valid_parameters() 38 | { 39 | $calculator = $this->shmock('Shmock\Even_Calculator', function ($calculator) { 40 | $calculator->raise_to_even(5)->return_value(6); 41 | }); 42 | 43 | $this->assertEquals(6, $calculator->raise_to_even(5)); 44 | } 45 | 46 | /** 47 | * @expectedException \Shmock\Shmock_Exception 48 | */ 49 | public function test_policy_prevents_odd_return_values() 50 | { 51 | $calculator = $this->shmock('Shmock\Even_Calculator', function ($calculator) { 52 | $calculator->raise_to_even(5)->any()->return_value(7); 53 | }); 54 | 55 | $this->fail("should not reach here"); 56 | } 57 | 58 | /** 59 | * @expectedException \Shmock\Shmock_Exception 60 | */ 61 | public function test_policy_prevents_unexpected_throws() 62 | { 63 | $calculator = $this->shmock('Shmock\Even_Calculator', function ($calculator) { 64 | $calculator->raise_to_even(5)->any()->throw_exception(new \Exception()); 65 | }); 66 | 67 | $this->fail("should not reach here"); 68 | } 69 | 70 | /** 71 | * @expectedException \InvalidArgumentException 72 | */ 73 | public function test_policy_allows_valid_throws() 74 | { 75 | $calculator = $this->shmock('Shmock\Even_Calculator', function ($calculator) { 76 | $calculator->raise_to_even(5)->throw_exception(new \InvalidArgumentException()); 77 | }); 78 | 79 | $calculator->raise_to_even(5); 80 | } 81 | 82 | /** 83 | * @expectedException \InvalidArgumentException 84 | */ 85 | public function test_arg_policy_throws_when_no_args_passed() 86 | { 87 | $this->shmock('Shmock\Even_Calculator', function ($calculator) { 88 | $calculator->raise_to_even(); 89 | }); 90 | } 91 | 92 | public function test_arg_policy_does_not_throw_when_no_args_passed_and_escape_flag_set() 93 | { 94 | Shmock::$check_args_for_policy_on_instance_method_when_no_args_passed = false; 95 | $calc = $this->shmock('Shmock\Even_Calculator', function ($calculator) { 96 | $calculator->raise_to_even()->return_value(6); 97 | }); 98 | Shmock::$check_args_for_policy_on_instance_method_when_no_args_passed = true; 99 | $this->assertEquals($calc->raise_to_even(13), 6); 100 | } 101 | 102 | /** 103 | * @expectedException \UnexpectedValueException 104 | */ 105 | public function test_return_this_works_with_policies() 106 | { 107 | $calc = $this->shmock('Shmock\Even_Calculator', function ($calculator) { 108 | $calculator->raise_to_even(7)->return_this(); 109 | }); 110 | $calc->raise_to_even(7); 111 | } 112 | } 113 | 114 | class Even_Calculator 115 | { 116 | /** 117 | * Should always return an even number 118 | */ 119 | public function raise_to_even($value) 120 | { 121 | if (!is_integer($value)) { 122 | throw new \InvalidArgumentException("$value"); 123 | } 124 | if ($value % 2 != 0) { 125 | return $value + 1; 126 | } 127 | 128 | return $value; 129 | } 130 | } 131 | 132 | /** 133 | * When mocking the Even_Calculator, we want the mock objects to be subject to the 134 | * same contracts that would be enforced at runtime. 135 | */ 136 | class Even_Number_Policy extends Policy 137 | { 138 | public function check_method_parameters($class, $method, $parameters, $static) 139 | { 140 | if (count($parameters) == 0) { 141 | throw new \InvalidArgumentException("No args passed!"); 142 | } 143 | 144 | if (!is_integer($parameters[0])) { 145 | throw new Shmock_Exception(); 146 | } 147 | } 148 | 149 | public function check_method_return_value($class, $method, $return_value, $static) 150 | { 151 | if (is_object($return_value)) { 152 | throw new \UnexpectedValueException("object returned?!?"); 153 | } 154 | if ($return_value % 2 === 1) { 155 | throw new Shmock_Exception(); 156 | } 157 | } 158 | 159 | public function check_method_throws($class, $method, $exception, $static) 160 | { 161 | if (!($exception instanceof \InvalidArgumentException)) { 162 | throw new Shmock_Exception(); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shmock (SHorthand for MOCKing) 2 | 3 | ## What is this? 4 | 5 | Shmock is a smooth alternative for creating mocks with PHPUnit that uses the mock/replay concept from EasyMock but uses closures to define the scope for mocking. 6 | 7 | ```php 8 | incrementing_service = $incrementing_service; 22 | } 23 | 24 | public function next_foo() 25 | { 26 | $this->foo = $this->incrementing_service->increment($this->foo); 27 | return $this->foo; 28 | } 29 | } 30 | 31 | /** 32 | * Our test case runs the same test case twice - once with the original PHPUnit mocking 33 | * syntax and a second time with Shmock syntax. 34 | */ 35 | class Foo_Test extends PHPUnit_Framework_TestCase 36 | { 37 | use \Shmock\Shmockers; // This enables the use of the Shmock helper methods (replicated below) 38 | 39 | public function test_phpunit_original_mocking_syntax() 40 | { 41 | // this is the original PHPUnit mock syntax 42 | 43 | $incrementing_service_mock = $this->getMock('\Foo\Incrementing_Service', array('increment')); 44 | $incrementing_service_mock->expects($this->once()) 45 | ->method('increment') 46 | ->with($this->equalTo(0)) 47 | ->will($this->returnValue(1)); 48 | 49 | $foo = new Foo($incrementing_service_mock); 50 | $this->assertEquals(1, $foo->next_foo(0)); 51 | } 52 | 53 | /** 54 | * Create a shmock representation for $class_name and configure expected 55 | * mock interaction with $conf_closure 56 | * @return Shmock A fully configured mock object 57 | * @note You do not need this protected method if you use the Shmockers trait, shown above 58 | */ 59 | protected function shmock($class_name, $conf_closure) 60 | { 61 | return \Shmock\Shmock::create_class($this, $class_name, $conf_closure); 62 | } 63 | 64 | public function test_shmock_syntax() 65 | { 66 | // here's shmock. Neat huh? 67 | $incrementing_service_mock = $this->shmock('\Foo\Incrementing_Service', function($shmock) 68 | { 69 | $shmock->increment(0)->return_value(1); 70 | }); 71 | 72 | $foo = new Foo($incrementing_service_mock); 73 | $this->assertEquals(1, $foo->next_foo(0)); 74 | } 75 | } 76 | ``` 77 | ## Installation 78 | 79 | Shmock can be installed directly from [Packagist](https://packagist.org/packages/box/shmock). 80 | 81 | ```json 82 | "require": { 83 | "box/shmock": "1.0.0.x-dev" 84 | } 85 | ``` 86 | Alternatively you can download Shmock.php and Shmockers.php into your test directory and run 87 | 88 | ``` 89 | require_once 'Shmock.php'; 90 | ``` 91 | PHPUnit should already be on the load path for this to work. 92 | 93 | ## Type Safety 94 | 95 | Shmock is typesafe by default and will attempt to tell you when you're using the wrong mocking approach. Shmock will throw errors in cases where: 96 | 97 | * You mock a static method as though it were an instance method or vice versa. 98 | * You mock a private method as though it were protected or public. 99 | * You mock a method that does not exist _and_ there is no __call / __callStatic magic method provided. 100 | 101 | These checks can be disabled by calling `$mock_object->disable_strict_method_checking()` inside the shmock closure. We also plan on supporting parameter and return value checking if it complies with yet-to-be-defined PHPDoc conventions. 102 | 103 | ## Documentation: 104 | 105 | [http://box.github.io/shmock/namespaces/Shmock.html](http://box.github.io/shmock/namespaces/Shmock.html) 106 | 107 | ## Full list of Shmock features: 108 | ```php 109 | shmock('\Foo\Incrementing_Service', function($my_class_shmock) // [1] 112 | { 113 | $my_class_shmock->no_args_method(); // [2] 114 | $my_class_shmock->two_arg_method('param1', 'param2'); // [3] 115 | $my_class_shmock->method_that_returns_a_number()->return_value(100); // [4] 116 | $my_class_shmock->method_that_gets_run_twice()->times(2); // [5] 117 | $my_class_shmock->method_that_gets_run_any_times()->any(); // [6] 118 | 119 | $my_class_shmock->method_puts_it_all_together('with', 'args')->times(2)->return_value(false); 120 | 121 | $my_class_shmock->method_returns_another_mock()->return_shmock('\Another_Namespace\Another_Class', function($another_class) // [7] 122 | { 123 | $another_class->order_matters(); // [8] 124 | $another_class->disable_original_constructor(); // [9a] 125 | $another_class->set_constructor_arguments(1, 'Foo'); // [9b] 126 | 127 | $another_class->method_dies_horribly()->throw_exception(new InvalidArgumentException()); // [10] 128 | 129 | $another_class->method_gets_stubbed(1,2)->will(function(PHPUnit_Framework_MockObject_Invocation $invocation) 130 | { 131 | $a = $invocation->parameters[0]; 132 | $b = $invocation->parameters[1]; 133 | return $a + $b; // [11] 134 | }); 135 | }); 136 | 137 | $my_class_shmock->shmock_class(function($Inc_Service) 138 | { 139 | $Inc_Service->my_static_method()->any()->return_value('This was returned inside the mock instance using the static:: prefix'); // [12] 140 | }); 141 | 142 | }) 143 | 144 | $another_class = $this->shmock_class('\Another_Namespace\Another_Class', function($Another_Class) // [13] 145 | { 146 | $Another_Class->a_static_method()->return_value(1); 147 | }); 148 | ``` 149 | 150 | 1. Shmock lets you configure a mock object inside a closure. You work with a proxy object that feels like the real thing. 151 | 2. Invoking a method sets up the expectation that it will be called once. 152 | 3. Invoking a method with arguments causes it to expect those arguments when actually invoked. 153 | 4. You can return values from specific invocations. In the example, the value 100 will be returned when you call the method. 154 | 5. You can specify an expectation for the number of times a method will be called. By default it's expected once. 155 | 6. Or you can specify "0 or more" times with any() 156 | 7. You can nest your Shmock invocations, letting you define your mock dependencies elegantly. (If you have a two-way dependency, you can always just `return_value($other_shmock)` and define it somewhere else ) 157 | 8. On an object-level you can specify "order matters", meaning that the ordering of function invocations should be asserted against as well. Under the hood, this uses PHPUnit's `at(N)` calls automatically 158 | 9. You have some options as far as defining constructor arguments. a) You can opt to disable the original constructor. Normally PHPUnit will run the original constructor. b) You can run the original constructor with the given arguments. 159 | 10. Instead of returning a value, you can throw an exception when a method gets called. 160 | 11. Even more sophisticated, you can execute an arbitrary closure when the function gets called. 161 | 12. If you want to mock static functions, you call `shmock_class` which will give you all the same Shmock semantics as instances (where it makes sense). This is particularly useful when you want to partially mock an object, keeping some of the original behavior, but mocking out static / protected methods that may exist that the method you are testing is dependent on. 162 | 13. You can also mock a class independently of a mock instance. 163 | 164 | ## Copyright and License 165 | 166 | Copyright 2014 Box, Inc. All rights reserved. 167 | 168 | Licensed under the Apache License, Version 2.0 (the "License"); 169 | you may not use this file except in compliance with the License. 170 | You may obtain a copy of the License at 171 | 172 | http://www.apache.org/licenses/LICENSE-2.0 173 | 174 | Unless required by applicable law or agreed to in writing, software 175 | distributed under the License is distributed on an "AS IS" BASIS, 176 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 177 | See the License for the specific language governing permissions and 178 | limitations under the License. -------------------------------------------------------------------------------- /src/Shmock/Shmock.php: -------------------------------------------------------------------------------- 1 | 53 | * // build a mock of MyCalculator, expecting a call to add 54 | * // with arguments [1,2] and return the value 3, exactly once. 55 | * 56 | * $mock = \Shmock\Shmock::create($this, 'MyCalculator', function ($calc) { 57 | * $calc->add(1,2)->return_value(3); 58 | * }); 59 | * 60 | * 61 | * In the example above, the invocation target of the method add(1,2) 62 | * is an of \Shmock\Instance. This instance will allow you to mock any 63 | * instance method on the class MyCalculator, so it might allow add or subtract, 64 | * but not openFileStream() or sbutract. The result of the method 65 | * is an instance of \Shmock\Spec, which contains many of the familiar 66 | * expectation-setting methods for mock frameworks. 67 | * 68 | * You may easily design your own build / replay lifecycle to meet your needs by 69 | * using the Instance and StaticClass classes directly. 70 | * 71 | *
 72 |      * $shmock = new \Shmock\Instance($this, 'MyCalculator');
 73 |      * $shmock->add(1,2)->return_value(3);
 74 |      * $mock = $shmock->replay();
 75 |      * 
76 | * 77 | * @param \PHPUnit_Framework_TestCase $test_case 78 | * @param string $class the class being mocked 79 | * @param callable $closure the build phase of the mock 80 | * @return mixed An instance of a subclass of $class. PHPUnit mocks require that all mocks 81 | * be subclasses of the target class in order to replace target methods. For this reason, mocking 82 | * will fail if the class is final. 83 | * @see \Shmock\Instance \Shmock\Instance 84 | * @see \Shmock\Class \Shmock\StaticClass 85 | * @see \Shmock\Spec See \Shmock\Spec to get a sense of what methods are available for setting expectations. 86 | * @see \Shmock\Shmockers See the Shmockers trait for a shorthand helper to use in test cases. 87 | */ 88 | public static function create(\PHPUnit_Framework_TestCase $test_case, $class, callable $closure) 89 | { 90 | $shmock = new ClassBuilderInstanceClass($test_case, $class); 91 | self::$outstanding_shmocks[] = $shmock; 92 | if ($closure) { 93 | $closure($shmock); 94 | } 95 | 96 | return $shmock->replay(); 97 | } 98 | 99 | /** 100 | * Create a mock class. Mock classes go through the build / replay lifecycle like mock instances do. 101 | * @param \PHPUnit_Framework_TestCase $test_case 102 | * @param string $class the class to be mocked 103 | * @param callable $closure the closure to apply to the class mock in its build phase. 104 | * @return string a subclass of $class that has mock expectations set on it. 105 | * @see \Shmock\Shmock::create() 106 | */ 107 | public static function create_class($test_case, $class, $closure) 108 | { 109 | $shmock_class = new ClassBuilderStaticClass($test_case, $class); 110 | self::$outstanding_shmocks[] = $shmock_class; 111 | if ($closure) { 112 | $closure($shmock_class); 113 | } 114 | 115 | return $shmock_class->replay(); 116 | } 117 | 118 | /** 119 | * Add a policy to Shmock that ensures qualities about mock objects as they are created. Policies 120 | * allow you to highly customize the behavior of Shmock. 121 | * @param \Shmock\Policy $policy 122 | * @return void 123 | * @see \Shmock\Policy See \Shmock\Policy for documentation on how to create custom policies. 124 | */ 125 | public static function add_policy(Policy $policy) 126 | { 127 | self::$policies[] = $policy; 128 | } 129 | 130 | /** 131 | * Clears any set policies. 132 | * @return void 133 | * @see \Shmock\Policy See \Shmock\Policy for documentation on how to create custom policies. 134 | */ 135 | public static function clear_policies() 136 | { 137 | self::$policies = []; 138 | } 139 | 140 | /** 141 | * This will verify that all mocks so far have been satisfied. It will clear 142 | * the set of outstanding mocks, regardless if any have failed. 143 | * @return void 144 | */ 145 | public static function verify() 146 | { 147 | $mocks = self::$outstanding_shmocks; 148 | self::$outstanding_shmocks = []; 149 | foreach ($mocks as $mock) { 150 | $mock->verify(); 151 | } 152 | } 153 | 154 | } 155 | 156 | /** 157 | * This is used to support the will() response to mocking a method. 158 | * @internal 159 | */ 160 | class Shmock_Closure_Invoker implements \PHPUnit_Framework_MockObject_Stub 161 | { 162 | /** @var callable */ 163 | private $closure = null; 164 | 165 | /** 166 | * @internal 167 | * @param callable 168 | */ 169 | public function __construct($closure) 170 | { 171 | $this->closure = $closure; 172 | } 173 | 174 | /** 175 | * @internal 176 | * @param \PHPUnit_Framework_MockObject_Invocation $invocation 177 | * @return mixed|null the result of the invocation 178 | */ 179 | public function invoke(\PHPUnit_Framework_MockObject_Invocation $invocation) 180 | { 181 | $fn = $this->closure; 182 | 183 | return $fn($invocation); 184 | } 185 | 186 | /** 187 | * @internal 188 | * @return string 189 | */ 190 | public function toString() 191 | { 192 | return "Closure invoker"; 193 | } 194 | } 195 | 196 | /** 197 | * It is recommended for Shmock policy implementors to use the \Shmock\Shmock_Exception type 198 | * to signal policy infringements. 199 | * @see \Shmock\Policy Documentation on policies 200 | */ 201 | class Shmock_Exception extends \Exception {} 202 | -------------------------------------------------------------------------------- /src/Shmock/Spec.php: -------------------------------------------------------------------------------- 1 | 15 | * // expect notify will be called 5 times 16 | * $shmock->notify()->times(5); 17 | * 18 | * 19 | * @param int $times the number of times to expect the given call 20 | * @return \Shmock\Spec 21 | * @see \Shmock\Spec::at_least_once() See at_least_once() 22 | */ 23 | public function times($times); 24 | 25 | /** 26 | * Specify that the method will be invoked once. 27 | * 28 | * This is a shorthand for times(1) 29 | * @return \Shmock\Spec 30 | * @see \Shmock\Spec::times() See times() 31 | */ 32 | public function once(); 33 | 34 | /** 35 | * Specify that the method will be invoked twice. 36 | * 37 | * This is a shorthand for times(2) 38 | * @return \Shmock\Spec 39 | * @see \Shmock\Spec::times() See times() 40 | */ 41 | public function twice(); 42 | 43 | /** 44 | * Specifies that the number of invocations of this method 45 | * is not to be verified by Shmock. 46 | *
 47 |      *  $shmock->notify()->any();
 48 |      * 
49 | * This is a shorthand for times(null) 50 | * @return \Shmock\Spec 51 | * @see \Shmock\Spec::times() See times() 52 | */ 53 | public function any(); 54 | 55 | /** 56 | * Specifies that this method is never to be invoked. 57 | * 58 | * This is an alias for times(0) 59 | * @return \Shmock\Spec 60 | * @see \Shmock\Spec::times() See times() 61 | */ 62 | public function never(); 63 | 64 | /** 65 | * Specifies that the method is to be invoked at least once 66 | * but possibly more. 67 | * This directive is only respected if no other calls to times() have been recorded. 68 | *
 69 |      *  $shmock->notify()->at_least_once();
 70 |      * 
71 | * 72 | * @return \Shmock\Spec 73 | * @see \Shmock\Spec::times() See times() 74 | */ 75 | public function at_least_once(); 76 | 77 | /** 78 | * Specifies that the given closure will be executed on invocation. 79 | * The first argument to the closure is an instance of \PHPUnit_Framework_MockObject_Invocation. 80 | * 81 | *
 82 |      *  // custom action with a closure
 83 |      *
 84 |      *  $shmock->notify()->will(function ($invocation) {
 85 |      *    $this->assertTrue(count($invocation->parameters) > 2);
 86 |      *  });
 87 |      * 
88 | * 89 | * @param callable $will_closure 90 | * @return \Shmock\Spec 91 | */ 92 | public function will($will_closure); 93 | 94 | /** 95 | * An order-agnostic set of return values given a set of inputs. 96 | * 97 | * @param mixed[][] $map_of_args_to_values an array of arrays of arguments with the final value 98 | * of the array being the return value. 99 | * @return \Shmock\Spec 100 | * For example, if you were simulating addition: 101 | *
102 |     * $shmock_calculator->add()->return_value_map([
103 |     * 	[1, 2, 3], // 1 + 2 = 3
104 |     * 	[10, 15, 25],
105 |     * 	[11, 11, 22]
106 |     * ]);
107 |     * 
108 | */ 109 | public function return_value_map($map_of_args_to_values); 110 | 111 | /** 112 | * Specifies that the method will return true. 113 | * This is a shorthand for return_value(true) 114 | * @return \Shmock\Spec 115 | */ 116 | public function return_true(); 117 | 118 | /** 119 | * Specifies that the method will return false. 120 | * This is a shorthand for return_value(false) 121 | * @return \Shmock\Spec 122 | */ 123 | public function return_false(); 124 | 125 | /** 126 | * Specifies that the method will return null. 127 | * This is a shorthand for return_value(null) 128 | * @return \Shmock\Spec 129 | */ 130 | public function return_null(); 131 | 132 | /** 133 | * Specifies that the method will return the given value on invocation. 134 | *
135 |      *  $shmock->notify()->return_value("notification!");
136 |      * 
137 | * @param mixed|null $value The value to return on invocation 138 | * @return \Shmock\Spec 139 | * @see \Shmock\Instance::order_matters() If you wish to specify multiple return values and the order is important, look at Instance::order_matters() 140 | * @see \Shmock\Spec::return_value_map() If you wish to specify multiple return values contingent on the parameters, but otherwise insensitive to the order, look at return_value_map() 141 | */ 142 | public function return_value($value); 143 | 144 | /** 145 | * Specifies that the method will return the invocation target. This is 146 | * useful for mocking other objects that have fluent interfaces. 147 | *
148 |      *  $latte->add_foam()->return_this();
149 |      *  $latte->caffeine_free()->return_this();
150 |      * 
151 | * @return \Shmock\Spec 152 | */ 153 | public function return_this(); 154 | 155 | /** 156 | * Throws an exception on invocation. 157 | * @param \Exception|void $e the exception to throw. If not specified, Shmock will provide an instance of 158 | * the base \Exception. 159 | * @return \Shmock\Spec 160 | */ 161 | public function throw_exception($e=null); 162 | 163 | /** 164 | * Specifies that each subsequent invocation of this method will take the subsequent value from the array as the return value. 165 | * 166 | * The sequence of values to return is not affected by ordering constraints on the mock (ie, order_matters()). 167 | * 168 | *
169 |      *  $shmock->notify()->return_consecutively(["called!", "called again!", "called a third time!"]);
170 |      *
171 |      *  $mock = $shmock->replay(); // replay is automatically called at the end of \Shmock\Shmock::create()
172 |      *
173 |      *  $mock->notify(); // called!
174 |      *  $mock->notify(); // called again!
175 |      *  $mock->notify(); // called a third time!
176 |      * 
177 | * @param mixed[] $array_of_values the sequence of values to return. 178 | * @param boolean|void $keep_returning_last_value whether to continue returning the last element in the sequence 179 | * or to fail the count expectation after every sequence element has been used. Defaults to false. 180 | * @return \Shmock\Spec 181 | */ 182 | public function return_consecutively($array_of_values, $keep_returning_last_value=false); 183 | 184 | /** 185 | * Specifies that the return value from this function will be a new mock object, which is 186 | * built and replayed as soon as the invocation has occurred. 187 | * The signature of return_shmock() is similar to \Shmock\Shmock::create() 188 | * except that the test case argument is omitted 189 | * 190 | *
191 |      *  $user = \Shmock\Shmock::create($this, 'User', function ($user) {
192 |      *    $user->supervisor()->return_shmock('Supervisor', function ($supervisor) {
193 |      *      $supervisor->send_angry_email("I need you to work this weekend");
194 |      *    });
195 |      *  });
196 |      * 
197 | * @param string $class the name of the class to mock. 198 | * @param callable $shmock_closure a closure that will act as the class's build phase. 199 | * @return \Shmock\Spec 200 | */ 201 | public function return_shmock($class, $shmock_closure); 202 | 203 | /** 204 | * @internal invoked at the end of the build() phase 205 | * @param mixed $mock 206 | * @param \Shmock\Policy[] $policies 207 | * @param boolean $static 208 | * @param string the name of the class being mocked 209 | * @return void 210 | */ 211 | public function __shmock_finalize_expectations($mock, array $policies, $static, $class); 212 | 213 | /** 214 | * @return string the name of the specification 215 | */ 216 | public function __shmock_name(); 217 | 218 | /** 219 | * Verifies that the mock object had all expectations met 220 | * @throws \PHPUnit_Framework_AssertionFailedError 221 | * @return void 222 | */ 223 | public function __shmock_verify(); 224 | } 225 | -------------------------------------------------------------------------------- /src/Shmock/ClassBuilderStaticClass.php: -------------------------------------------------------------------------------- 1 | testCase = $testCase; 41 | $this->className = $className; 42 | } 43 | 44 | /** 45 | * @return void 46 | */ 47 | public function verify() 48 | { 49 | if ($this->ordering !== null) { 50 | $this->ordering->verify(); 51 | } 52 | } 53 | 54 | /** 55 | * @return \Shmock\Instance 56 | */ 57 | public function disable_original_constructor() 58 | { 59 | // no-op 60 | return $this; 61 | } 62 | 63 | /** 64 | * @param *mixed|null 65 | * @return \Shmock\Instance 66 | */ 67 | public function set_constructor_arguments() 68 | { 69 | // no-op 70 | return $this; 71 | } 72 | 73 | /** 74 | * @param bool|void whether to stub static methods 75 | * @return \Shmock\Instance 76 | */ 77 | public function dont_preserve_original_methods($stubStaticMethods = true) 78 | { 79 | $reflectionClass = new \ReflectionClass($this->className); 80 | $methods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); 81 | foreach ($methods as $method) { 82 | $stubMethod = $stubStaticMethods ? $method->isStatic() : !$method->isStatic(); 83 | if (!$method->isFinal() && $stubMethod) { 84 | $this->__call($method->getName(), [])->any()->return_null(); 85 | } 86 | } 87 | return $this; 88 | } 89 | 90 | /** 91 | * When this is called, Shmock will begin keeping track of the order of calls made on this 92 | * mock. This is implemented by using the PHPUnit at() feature and keeping an internal 93 | * counter to track order. 94 | * 95 | *
 96 |      *  $shmock->order_matters();
 97 |      *  $shmock->notify('first notification');
 98 |      *  $shmock->notify('second notification');
 99 |      * 
100 | * In this example, the string "first notification" is expected to be sent to notify first during replay. If 101 | * any other string, including "second notification" is received, it will fail the expectation. 102 | * 103 | * Shmock does not expose the at() feature directly. 104 | * @return \Shmock\Instance 105 | */ 106 | public function order_matters() 107 | { 108 | if ($this->ordering === null) { 109 | $this->ordering = new MethodNameOrdering(); 110 | } else if ($this->ordering instanceof Unordered) { 111 | $this->ordering = $this->ordering->convertToMethodNameOrdering(); 112 | } else { 113 | throw new \InvalidArgumentException("You cannot set the ordering constraint more than once. (It is implicitly set to 'unordered' after the first method is specified)"); 114 | } 115 | return $this; 116 | } 117 | 118 | /** 119 | * Disables order checking. Note that order is already disabled by default, so this does not need 120 | * to be invoked unless order_matters was previously invoked 121 | * @see \Shmock\Instance::order_matters() See order_matters() to trigger order enforcement 122 | * @return \Shmock\Instance 123 | */ 124 | public function order_doesnt_matter() 125 | { 126 | if ($this->ordering !== null && !($this->ordering instanceof Unordered)) { 127 | throw new \InvalidArgumentException("You cannot set the ordering constraint more than once! The ordering constraint is implicity set to 'unordered' after the first method is specified."); 128 | } 129 | return $this; 130 | } 131 | 132 | /** 133 | * Helper method to properly initialize a class builder with everything 134 | * ready for $builder->create() to be invoked. Has no side effects 135 | * 136 | * @return \Shmock\ClassBuilder\ClassBuilder 137 | */ 138 | protected function initializeClassBuilder() 139 | { 140 | // build the mock class 141 | $builder = new ClassBuilder(); 142 | if (class_exists($this->className)) { 143 | $builder->setExtends($this->className); 144 | } elseif (interface_exists($this->className)) { 145 | $builder->addInterface($this->className); 146 | } else { 147 | throw new \InvalidArgumentException("Class or interface " . $this->className . " does not exist"); 148 | } 149 | 150 | // every mocked method goes through this invocation, which delegates 151 | // the retrieval of the correct Spec to this Instance's Ordering constraint. 152 | $resolveCall = function (Invocation $inv) { 153 | $spec = $this->ordering->nextSpec($inv->getMethodName()); 154 | return $spec->doInvocation($inv); 155 | }; 156 | 157 | return $this->addMethodsToBuilder($builder, $resolveCall); 158 | } 159 | 160 | /** 161 | * @return mixed The mock object, now in its replay phase. 162 | */ 163 | public function replay() 164 | { 165 | return $this->initializeClassBuilder()->create(); 166 | } 167 | 168 | /** 169 | * Helper function to add all called methods to the class builder 170 | * 171 | * @param \Shmock\ClassBuilder\ClassBuilder $builder 172 | * @param callable $resolveCall 173 | * @return \Shmock\ClassBuilder\ClassBuilder 174 | */ 175 | protected function addMethodsToBuilder(ClassBuilder $builder, callable $resolveCall) 176 | { 177 | foreach (array_unique($this->expectedStaticMethodCalls) as $methodCall) { 178 | $inspector = new MethodInspector($this->className, $methodCall); 179 | $builder->addStaticMethod($methodCall, $resolveCall, $inspector->signatureArgs()); 180 | } 181 | return $builder; 182 | } 183 | 184 | /** 185 | * Shmock intercepts all non-shmock methods here. 186 | * 187 | * Shmock will fail the test if any of the following are true: 188 | * 189 | *
    190 | *
  1. The class being mocked doesn't exist.
  2. 191 | *
  3. The method being mocked doesn't exist AND there is no __call handler on the class.
  4. 192 | *
  5. The method is private.
  6. 193 | *
  7. The method is static. (or non-static if using a StaticClass )
  8. 194 | *
195 | * 196 | * Additionally, any expectations set by Shmock policies may trigger an exception when replay() is invoked. 197 | * @param string $methodName the method on the target class 198 | * @param array $with the arguments to the mocked method 199 | * @return \Shmock\Spec a spec that can add additional constraints to the invocation. 200 | * @see \Shmock\Spec See \Shmock\Spec for additional constraints that can be placed on an invocation 201 | */ 202 | public function __call($methodName, $with) 203 | { 204 | if ($this->ordering === null) { 205 | $this->ordering = new Unordered(); 206 | } 207 | $spec = $this->initSpec($methodName, $with); 208 | $this->ordering->addSpec($methodName, $spec); 209 | $this->recordMethodInvocation($methodName); 210 | return $spec; 211 | } 212 | 213 | /** 214 | * Housekeeping function to record a method invocation 215 | * 216 | * @param string $methodName 217 | * @return void 218 | */ 219 | protected function recordMethodInvocation($methodName) 220 | { 221 | $this->expectedStaticMethodCalls[] = $methodName; 222 | } 223 | 224 | /** 225 | * Build a spec object given the method and args 226 | * @param string $methodName 227 | * @param array $with 228 | * @return Spec 229 | */ 230 | protected function initSpec($methodName, array $with) 231 | { 232 | return new StaticSpec($this->testCase, $this->className, $methodName, $with, Shmock::$policies); 233 | } 234 | 235 | /** 236 | * Mocks the object's underlying static methods. 237 | * @param callable 238 | * @return void 239 | */ 240 | public function shmock_class($closure) 241 | { 242 | throw new \BadMethodCallException("you are already mocking the class"); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/Shmock/ClassBuilderInstanceClass.php: -------------------------------------------------------------------------------- 1 | constructor_arguments)) { 76 | throw new \BadMethodCallException("You cannot disable the constructor after you have set constructor arguments!"); 77 | } 78 | $this->disable_original_constructor = true; 79 | return $this; 80 | } 81 | 82 | /** 83 | * Any arguments passed in here will be included in the 84 | * constructor call for the mocked class. 85 | * @param *mixed|null Arguments to the target constructor 86 | * @return \Shmock\Instance 87 | */ 88 | public function set_constructor_arguments() 89 | { 90 | if ($this->disable_original_constructor) { 91 | throw new \BadMethodCallException("You cannot set constructor arguments after you have disabled the constructor!"); 92 | } 93 | $this->constructor_arguments = func_get_args(); 94 | return $this; 95 | } 96 | 97 | /** 98 | * When this is called, Shmock will disable any of the original implementations 99 | * of methods on the mocked class. This can be useful when no expectations are set 100 | * on a particular method but the original implementation cannot be called in 101 | * testing. 102 | * @return \Shmock\Instance 103 | */ 104 | public function dont_preserve_original_methods() 105 | { 106 | return parent::dont_preserve_original_methods($this->in_static_mode); 107 | } 108 | 109 | /** 110 | * Helper method to properly initialize a class builder with everything 111 | * ready for $builder->create() to be invoked. Has no side effects 112 | * 113 | * @return \Shmock\ClassBuilder\ClassBuilder 114 | */ 115 | protected function initializeClassBuilder() 116 | { 117 | $builder = parent::initializeClassBuilder(); 118 | 119 | if ($this->disable_original_constructor) { 120 | $builder->disableConstructor(); 121 | } 122 | 123 | return $builder; 124 | } 125 | 126 | /** 127 | * When this is invoked, Shmock concludes this instance's build phase, runs 128 | * any policies that may have been registered, and creates a mock object in the 129 | * replay phase. 130 | * @return mixed an instance of a subclass of the mocked class. 131 | */ 132 | public function replay() 133 | { 134 | $mockClassName = parent::replay(); 135 | 136 | $mockClassReflector = new \ReflectionClass($mockClassName); 137 | return $mockClassReflector->newInstanceArgs($this->constructor_arguments); 138 | } 139 | 140 | /** 141 | * Helper function to add all called methods to the class builder 142 | * 143 | * @param \Shmock\ClassBuilder\ClassBuilder $builder 144 | * @param callable $resolveCall 145 | * @return \Shmock\ClassBuilder\ClassBuilder 146 | */ 147 | protected function addMethodsToBuilder(ClassBuilder $builder, callable $resolveCall) 148 | { 149 | foreach (array_unique($this->expectedInstanceMethodCalls) as $methodCall) { 150 | $inspector = new MethodInspector($this->className, $methodCall); 151 | $builder->addMethod($methodCall, $resolveCall, $inspector->signatureArgs()); 152 | } 153 | return parent::addMethodsToBuilder($builder, $resolveCall); 154 | } 155 | 156 | /** 157 | * When mocking an object instance, it may be desirable to mock static methods as well. Because 158 | * Shmock has strict rules that mock instances may only mock instance methods, to mock a static method 159 | * requires dropping into the mock class context. 160 | * 161 | * This is made simple by the shmock_class() method on Instance. 162 | * 163 | *
164 |      *   // User is an ActiveRecord-style class with finders and data members
165 |      *   // (this is just an example, this is probably not a good way to organize this code)
166 |      *   class User {
167 |      *      private $id;
168 |      *      private $userName;
169 |      *
170 |      *      public function updateUserName($userName) {
171 |      *         /// persist to db
172 |      *         $this->userName = $userName;
173 |      *         $handle = static::dbHandle();
174 |      *         $handle->update(['userName' => $this->userName]);
175 |      *         $this->fireEvent('/users/username');
176 |      *      }
177 |      *
178 |      *      public function fireEvent($eventType) {
179 |      *         Notifications::fire($this->id, $eventType);
180 |      *      }
181 |      *
182 |      *      public static function dbHandle() {
183 |      *        return new DbHandle('schema.users');
184 |      *      }
185 |      *   }
186 |      *
187 |      *   // In a test we want to ensure that save() will fire notifications
188 |      *   // and correctly persist to the database.
189 |      *   $mock = $this->shmock('User', function ($user) {
190 |      *
191 |      *      // ensure that the user will fire the event
192 |      *      $user->fireEvent('/users/username')->once();
193 |      *
194 |      *      // use shmock_class to mock the static method dbHandle()
195 |      *      $user->shmock_class(function ($user_class) {
196 |      *         $user_class->dbHandle()->return_value(new FakeDBHandle());
197 |      *      });
198 |      *   });
199 |      * 
200 | * @param callable $closure 201 | * @return void 202 | */ 203 | public function shmock_class($closure) 204 | { 205 | $this->in_static_mode = true; 206 | $closure($this); 207 | $this->in_static_mode = false; 208 | } 209 | 210 | /** 211 | * Build a spec object given the method and args 212 | * @param string $methodName 213 | * @param array $with 214 | * @return Spec 215 | */ 216 | protected function initSpec($methodName, array $with) 217 | { 218 | if ($this->in_static_mode) { 219 | return new StaticSpec($this->testCase, $this->className, $methodName, $with, Shmock::$policies); 220 | } else { 221 | return new InstanceSpec($this->testCase, $this->className, $methodName, $with, Shmock::$policies); 222 | } 223 | } 224 | 225 | /** 226 | * Housekeeping function to record a method invocation 227 | * 228 | * @param string $methodName 229 | * @return void 230 | */ 231 | protected function recordMethodInvocation($methodName) 232 | { 233 | if ($this->in_static_mode) { 234 | $this->expectedStaticMethodCalls[] = $methodName; 235 | } else { 236 | $this->expectedInstanceMethodCalls[] = $methodName; 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /test/Shmock/ShmockTest.php: -------------------------------------------------------------------------------- 1 | shmock_class(function ($Shmock_Foo) { 13 | $Shmock_Foo->weewee()->return_value(3); 14 | }); 15 | }); 16 | $this->assertEquals(3, $foo->lala()); 17 | } 18 | 19 | public function testFooClassCanBeStaticallyMockedWithOrderMatters() 20 | { 21 | $foo = Shmock::create($this, '\Shmock\Shmock_Foo', function ($foo) { 22 | $foo->order_matters(); 23 | $foo->shmock_class(function ($Shmock_Foo) { 24 | $Shmock_Foo->weewee()->return_value(3); 25 | }); 26 | $foo->bar(3)->return_value(7); 27 | $foo->for_value_map(7,3)->return_value(23); 28 | }); 29 | $this->assertEquals(23, $foo->staticSequentialCalls()); 30 | } 31 | 32 | public function testFooClassShouldBeAbleToStaticallyMockWeewee() 33 | { 34 | $Shmock_Foo = Shmock::create_class($this, '\Shmock\Shmock_Foo', function ($Shmock_Foo) { 35 | $Shmock_Foo->weewee()->return_value(6); 36 | }); 37 | $this->assertEquals(6, $Shmock_Foo::weewee()); 38 | } 39 | 40 | public function ignoreTestReturnValueMapStubbedTwiceCalledOnce() 41 | { 42 | $this->markTestSkipped('Not sure how to find out that it should have thrown an exception'); 43 | $foo = Shmock::create($this, '\Shmock\Shmock_Foo', function ($foo) { 44 | $foo->for_value_map()->return_value_map(array( 45 | array(1, 2, 3), 46 | array(2, 2, 3), 47 | )); 48 | }); 49 | 50 | $this->assertEquals(3, $foo->for_value_map(2, 2), 'value map busted'); 51 | } 52 | 53 | public function testReturnValueMapStubbedAndCalledTwice() 54 | { 55 | $foo = Shmock::create($this, '\Shmock\Shmock_Foo', function ($foo) { 56 | $foo->for_value_map()->return_value_map(array( 57 | array(1, 2, 3), 58 | array(2, 2, 3), 59 | )); 60 | }); 61 | 62 | $this->assertEquals(3, $foo->for_value_map(1, 2), 'value map busted'); 63 | $this->assertEquals(3, $foo->for_value_map(2, 2), 'value map busted'); 64 | } 65 | 66 | public function testMockingNoMethodsAtAllShouldPreserveOriginalMethods() 67 | { 68 | $foo = Shmock::create($this, '\Shmock\Shmock_Foo', function ($foo) { 69 | $foo->disable_original_constructor(); 70 | }); 71 | $this->assertEquals(5, $foo::weewee()); 72 | } 73 | 74 | public function testMockCanBeMadeOfAbstractClassIfAllMethodsAreDefined() 75 | { 76 | $foo = Shmock::create($this, '\Shmock\AbstractFoo', function ($foo) { 77 | $foo->bar()->return_value(1); 78 | }); 79 | 80 | $this->assertSame(1, $foo->bar()); 81 | } 82 | 83 | public function testClassMocksAndBeCreatedUsingCreateClassMethod() 84 | { 85 | $fooClass = Shmock::create_class($this, '\Shmock\Shmock_Foo', function ($fooClass) { 86 | $fooClass->weewee()->return_value(10); 87 | }); 88 | 89 | $this->assertEquals(10, $fooClass::weewee()); 90 | } 91 | 92 | public function testShmockVerifyWillAssertThatAllMocksCreatedHaveMetExpectations() 93 | { 94 | $fooClass = Shmock::create_class($this, '\Shmock\Shmock_Foo', function ($fooClass) { 95 | $fooClass->weewee()->twice()->return_value(10); 96 | }); 97 | 98 | try { 99 | Shmock::verify(); 100 | $this->fail("Expected verify to fail after failing to call the mock method"); 101 | } catch (\PHPUnit_Framework_AssertionFailedError $e) { 102 | } 103 | 104 | } 105 | 106 | public function testClassInstanceFieldIsAlwaysSetOnShmocksWithCtorDisabled() 107 | { 108 | $foo = Shmock::create($this, '\Shmock\Shmock_Foo', function ($mock) { 109 | $mock->disable_original_constructor(); 110 | }); 111 | 112 | $this->assertFalse(property_exists($foo, 'foo')); 113 | $this->assertTrue(is_string($foo->class)); 114 | } 115 | 116 | public function testWillClosureGetsProperParamsOnIt() 117 | { 118 | $foo = Shmock::create($this, '\Shmock\Shmock_Foo', function ($mock) { 119 | $mock->bar()->any()->will(function ($invocation) { 120 | $args = $invocation->parameters; 121 | return $args[0] * 4; 122 | }); 123 | }); 124 | 125 | $this->assertSame($foo->barPlusTwo(4), 18); 126 | $this->assertSame($foo->barPlusTwo(100), 402); 127 | } 128 | 129 | public function testUsersCanShmockNonExistentMagicMethods() 130 | { 131 | $foo = Shmock::create($this, '\Shmock\Shmock_Foo', function ($mock) { 132 | $mock->magic()->return_value(2); 133 | }); 134 | $this->assertSame($foo->doMagic(), 84); 135 | } 136 | 137 | public function testShmockCanMockInterfaces() 138 | { 139 | $foo = Shmock::create($this, '\Shmock\Empty_Foo', function ($mock) { 140 | $mock->foo()->return_value(42); 141 | }); 142 | $this->assertSame($foo->foo(), 42); 143 | } 144 | 145 | public function testShmockCanHandleOrderMattersNotFirst() 146 | { 147 | $foo = Shmock::create($this, '\Shmock\Shmock_Foo', function ($mock) { 148 | $mock->doMagic(); 149 | $mock->magic(); 150 | $mock->bar(42); 151 | $mock->lala(); 152 | $mock->order_matters(); 153 | }); 154 | $foo->sequentialCalls(); 155 | } 156 | 157 | public function testShmockCanHandleOrderMattersAndThrowExceptionWhenWrong() 158 | { 159 | $foo = Shmock::create($this, '\Shmock\Shmock_Foo', function ($mock) { 160 | $mock->magic(); 161 | $mock->doMagic(); 162 | $mock->bar(42); 163 | $mock->lala(); 164 | $mock->order_matters(); 165 | }); 166 | $thrown = false; 167 | try { 168 | $foo->sequentialCalls(); 169 | } catch (\PHPUnit_Framework_AssertionFailedError $e) { 170 | $thrown = true; 171 | } 172 | $this->assertTrue($thrown); 173 | 174 | $thrown = false; 175 | try { 176 | Shmock::verify(); 177 | } catch (\Exception $e) { 178 | $thrown = true; 179 | } 180 | $this->assertTrue($thrown); 181 | } 182 | 183 | public function testShmockCannotMockStaticPrivateMethodsNormally() 184 | { 185 | $testCaseShmock = Shmock::create( 186 | $this, 187 | '\PHPUnit_Framework_TestCase', 188 | function ($mock) { 189 | $mock->shmock_class(function ($smock) { 190 | $smock->assertFalse(true); // this would normally cause the test to fail, this ensures that it normally would happen 191 | }); 192 | } 193 | ); 194 | 195 | $foo = Shmock::create_class( 196 | $testCaseShmock, 197 | '\Shmock\Shmock_Foo', 198 | function ($mock) { 199 | $mock->sBar()->return_value(2); 200 | } 201 | ); 202 | 203 | $foo::sFoo(); 204 | } 205 | 206 | public function testShmockCanMockStaticPrivateMethodsWithEscapeFlag() 207 | { 208 | Shmock::$disable_strict_method_checks_for_static_methods = true; 209 | $foo = Shmock::create_class( 210 | $this, 211 | '\Shmock\Shmock_Foo', 212 | function ($mock) { 213 | $mock->sBar()->return_value(2); 214 | } 215 | ); 216 | Shmock::$disable_strict_method_checks_for_static_methods = false; 217 | 218 | $this->assertEquals($foo::sFoo(), 2); 219 | } 220 | 221 | public function tearDown() 222 | { 223 | Shmock::verify(); 224 | } 225 | } 226 | 227 | class Shmock_Foo 228 | { 229 | public function __construct() 230 | { 231 | $this->foo = "42"; 232 | } 233 | 234 | public static function sFoo() 235 | { 236 | return static::sBar(); 237 | } 238 | 239 | private static function sBar() 240 | { 241 | return 42; 242 | } 243 | 244 | public function lala() 245 | { 246 | return static::weewee(); 247 | } 248 | 249 | public function for_value_map($a, $b) 250 | { 251 | return $a * $b; 252 | } 253 | 254 | public static function weewee() 255 | { 256 | return 5; 257 | } 258 | 259 | public function barPlusTwo($a) 260 | { 261 | return $this->bar($a) + 2; 262 | } 263 | 264 | public function bar($a) 265 | { 266 | return $a * 2; 267 | } 268 | 269 | public function __call($name, $args) 270 | { 271 | if ($name == "magic") { 272 | return 42; 273 | } 274 | return null; 275 | } 276 | 277 | public function doMagic() 278 | { 279 | return 42*$this->magic(); 280 | } 281 | 282 | public function sequentialCalls() 283 | { 284 | $this->doMagic(); 285 | $this->magic(); 286 | $this->bar(42); 287 | $this->lala(); 288 | } 289 | 290 | public function staticSequentialCalls() 291 | { 292 | $a = $this->bar(static::weewee()); 293 | return $this->for_value_map($a, 3); 294 | } 295 | } 296 | 297 | abstract class AbstractFoo 298 | { 299 | abstract public function bar(); 300 | } 301 | 302 | interface Empty_Foo 303 | { 304 | public function foo(); 305 | } 306 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | "License" shall mean the terms and conditions for use, reproduction, 9 | and distribution as defined by Sections 1 through 9 of this document. 10 | "Licensor" shall mean the copyright owner or entity authorized by 11 | the copyright owner that is granting the License. 12 | "Legal Entity" shall mean the union of the acting entity and all 13 | other entities that control, are controlled by, or are under common 14 | control with that entity. For the purposes of this definition, 15 | "control" means (i) the power, direct or indirect, to cause the 16 | direction or management of such entity, whether by contract or 17 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 18 | outstanding shares, or (iii) beneficial ownership of such entity. 19 | "You" (or "Your") shall mean an individual or Legal Entity 20 | exercising permissions granted by this License. 21 | "Source" form shall mean the preferred form for making modifications, 22 | including but not limited to software source code, documentation 23 | source, and configuration files. 24 | "Object" form shall mean any form resulting from mechanical 25 | transformation or translation of a Source form, including but 26 | not limited to compiled object code, generated documentation, 27 | and conversions to other media types. 28 | "Work" shall mean the work of authorship, whether in Source or 29 | Object form, made available under the License, as indicated by a 30 | copyright notice that is included in or attached to the work 31 | (an example is provided in the Appendix below). 32 | "Derivative Works" shall mean any work, whether in Source or Object 33 | form, that is based on (or derived from) the Work and for which the 34 | editorial revisions, annotations, elaborations, or other modifications 35 | represent, as a whole, an original work of authorship. For the purposes 36 | of this License, Derivative Works shall not include works that remain 37 | separable from, or merely link (or bind by name) to the interfaces of, 38 | the Work and Derivative Works thereof. 39 | "Contribution" shall mean any work of authorship, including 40 | the original version of the Work and any modifications or additions 41 | to that Work or Derivative Works thereof, that is intentionally 42 | submitted to Licensor for inclusion in the Work by the copyright owner 43 | or by an individual or Legal Entity authorized to submit on behalf of 44 | the copyright owner. For the purposes of this definition, "submitted" 45 | means any form of electronic, verbal, or written communication sent 46 | to the Licensor or its representatives, including but not limited to 47 | communication on electronic mailing lists, source code control systems, 48 | and issue tracking systems that are managed by, or on behalf of, the 49 | Licensor for the purpose of discussing and improving the Work, but 50 | excluding communication that is conspicuously marked or otherwise 51 | designated in writing by the copyright owner as "Not a Contribution." 52 | "Contributor" shall mean Licensor and any individual or Legal Entity 53 | on behalf of whom a Contribution has been received by Licensor and 54 | subsequently incorporated within the Work. 55 | 56 | 2. Grant of Copyright License. Subject to the terms and conditions of 57 | this License, each Contributor hereby grants to You a perpetual, 58 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 59 | copyright license to reproduce, prepare Derivative Works of, 60 | publicly display, publicly perform, sublicense, and distribute the 61 | Work and such Derivative Works in Source or Object form. 62 | 63 | 3. Grant of Patent License. Subject to the terms and conditions of 64 | this License, each Contributor hereby grants to You a perpetual, 65 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 66 | (except as stated in this section) patent license to make, have made, 67 | use, offer to sell, sell, import, and otherwise transfer the Work, 68 | where such license applies only to those patent claims licensable 69 | by such Contributor that are necessarily infringed by their 70 | Contribution(s) alone or by combination of their Contribution(s) 71 | with the Work to which such Contribution(s) was submitted. If You 72 | institute patent litigation against any entity (including a 73 | cross-claim or counterclaim in a lawsuit) alleging that the Work 74 | or a Contribution incorporated within the Work constitutes direct 75 | or contributory patent infringement, then any patent licenses 76 | granted to You under this License for that Work shall terminate 77 | as of the date such litigation is filed. 78 | 79 | 4. Redistribution. You may reproduce and distribute copies of the 80 | Work or Derivative Works thereof in any medium, with or without 81 | modifications, and in Source or Object form, provided that You 82 | meet the following conditions: 83 | 84 | (a) You must give any other recipients of the Work or 85 | Derivative Works a copy of this License; and 86 | 87 | (b) You must cause any modified files to carry prominent notices 88 | stating that You changed the files; and 89 | 90 | (c) You must retain, in the Source form of any Derivative Works 91 | that You distribute, all copyright, patent, trademark, and 92 | attribution notices from the Source form of the Work, 93 | excluding those notices that do not pertain to any part of 94 | the Derivative Works; and 95 | 96 | (d) If the Work includes a "NOTICE" text file as part of its 97 | distribution, then any Derivative Works that You distribute must 98 | include a readable copy of the attribution notices contained 99 | within such NOTICE file, excluding those notices that do not 100 | pertain to any part of the Derivative Works, in at least one 101 | of the following places: within a NOTICE text file distributed 102 | as part of the Derivative Works; within the Source form or 103 | documentation, if provided along with the Derivative Works; or, 104 | within a display generated by the Derivative Works, if and 105 | wherever such third-party notices normally appear. The contents 106 | of the NOTICE file are for informational purposes only and 107 | do not modify the License. You may add Your own attribution 108 | notices within Derivative Works that You distribute, alongside 109 | or as an addendum to the NOTICE text from the Work, provided 110 | that such additional attribution notices cannot be construed 111 | as modifying the License. 112 | 113 | You may add Your own copyright statement to Your modifications and 114 | may provide additional or different license terms and conditions 115 | for use, reproduction, or distribution of Your modifications, or 116 | for any such Derivative Works as a whole, provided Your use, 117 | reproduction, and distribution of the Work otherwise complies with 118 | the conditions stated in this License. 119 | 120 | 5. Submission of Contributions. Unless You explicitly state otherwise, 121 | any Contribution intentionally submitted for inclusion in the Work 122 | by You to the Licensor shall be under the terms and conditions of 123 | this License, without any additional terms or conditions. 124 | Notwithstanding the above, nothing herein shall supersede or modify 125 | the terms of any separate license agreement you may have executed 126 | with Licensor regarding such Contributions. 127 | 128 | 6. Trademarks. This License does not grant permission to use the trade 129 | names, trademarks, service marks, or product names of the Licensor, 130 | except as required for reasonable and customary use in describing the 131 | origin of the Work and reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. Unless required by applicable law or 134 | agreed to in writing, Licensor provides the Work (and each 135 | Contributor provides its Contributions) on an "AS IS" BASIS, 136 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 137 | implied, including, without limitation, any warranties or conditions 138 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 139 | PARTICULAR PURPOSE. You are solely responsible for determining the 140 | appropriateness of using or redistributing the Work and assume any 141 | risks associated with Your exercise of permissions under this License. 142 | 143 | 8. Limitation of Liability. In no event and under no legal theory, 144 | whether in tort (including negligence), contract, or otherwise, 145 | unless required by applicable law (such as deliberate and grossly 146 | negligent acts) or agreed to in writing, shall any Contributor be 147 | liable to You for damages, including any direct, indirect, special, 148 | incidental, or consequential damages of any character arising as a 149 | result of this License or out of the use or inability to use the 150 | Work (including but not limited to damages for loss of goodwill, 151 | work stoppage, computer failure or malfunction, or any and all 152 | other commercial damages or losses), even if such Contributor 153 | has been advised of the possibility of such damages. 154 | 155 | 9. Accepting Warranty or Additional Liability. While redistributing 156 | the Work or Derivative Works thereof, You may choose to offer, 157 | and charge a fee for, acceptance of support, warranty, indemnity, 158 | or other liability obligations and/or rights consistent with this 159 | License. However, in accepting such obligations, You may act only 160 | on Your own behalf and on Your sole responsibility, not on behalf 161 | of any other Contributor, and only if You agree to indemnify, 162 | defend, and hold each Contributor harmless for any liability 163 | incurred by, or claims asserted against, such Contributor by reason 164 | of your accepting any such warranty or additional liability. 165 | 166 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /src/Shmock/ClassBuilder/ClassBuilder.php: -------------------------------------------------------------------------------- 1 | className || class_exists($this->className)) { 67 | $this->className = "ClassBuilder" . $this->randStr(); 68 | } 69 | } 70 | 71 | /** 72 | * @return string the name of the class that is being created 73 | */ 74 | public function create() 75 | { 76 | $classTemplate = << 78 | { 79 | /* trait inclusions */ 80 | 81 | 82 | /* optional constructor implementation */ 83 | 84 | 85 | /* method implementations */ 86 | private static \$__implementations__ = []; 87 | 88 | private static \$__decorators__ = []; 89 | 90 | /** 91 | * @param string 92 | * @param callable 93 | * @return void 94 | */ 95 | public static function __add_implementation__(\$name, \$fn) 96 | { 97 | self::\$__implementations__[\$name] = \$fn; 98 | } 99 | 100 | /** 101 | * @param \Shmock\ClassBuilder\Decorator[] \$decorator 102 | * @return void 103 | */ 104 | public static function __set_decorators__(array \$decorators) 105 | { 106 | self::\$__decorators__ = \$decorators; 107 | } 108 | 109 | 110 | } 111 | EOF; 112 | $engine = new SprintfEngine("<", ">"); 113 | 114 | $functions = array_map(function ($method) { 115 | return $method->render(); 116 | }, $this->methods); 117 | 118 | $classDef = $engine->render($classTemplate, [ 119 | "className" => $this->className, 120 | "uses" => implode($this->traits, "\n"), 121 | "implements" => $this->interfaceStr(), 122 | "methods" => implode($functions, "\n"), 123 | "extends" => $this->extends, 124 | "constructor" => $this->constructor, 125 | ]); 126 | 127 | eval($classDef); 128 | 129 | foreach ($this->methods as $method) { 130 | $method->addToBuiltClass($this->className); 131 | } 132 | $clazz = $this->className; 133 | $clazz::__set_decorators__($this->decorators); 134 | 135 | return $this->className; 136 | 137 | } 138 | 139 | /** 140 | * @internal 141 | * @return string 142 | */ 143 | private function interfaceStr() 144 | { 145 | if (count($this->interfaces) == 0) { 146 | return ""; 147 | } 148 | 149 | return " implements " . implode($this->interfaces, ","); 150 | } 151 | 152 | /** 153 | * @param callable $decorator the decorator for all functions on this class 154 | * @return void 155 | */ 156 | public function addDecorator(callable $decorator) 157 | { 158 | $this->decorators[] = new CallableDecorator($decorator); 159 | } 160 | 161 | /** 162 | * @param string $methodName the method name 163 | * @param callable $fn the implementation of the method. The first argument to the callable is an Invocation object. 164 | * @param string[]|null|void $hints the hints for the method. If null, will attempt to detect the hints on $fn 165 | * @param string|void $accessLevel the access type, defaults to public 166 | * @return void 167 | */ 168 | public function addMethod($methodName, callable $fn, array $hints, $accessLevel = "public") 169 | { 170 | $this->methods[] = new Method($accessLevel, $methodName, $hints, $fn); 171 | } 172 | 173 | /** 174 | * @param string $methodName the method name 175 | * @param callable $fn the implementation of the method 176 | * @param string[]|null|void $hints the hints for the method. If null, will attempt to detect the hints on $fn 177 | * @param string|void the access level, defaults to public 178 | * @return void 179 | */ 180 | public function addStaticMethod($methodName, callable $fn, array $hints, $accessLevel = "public") 181 | { 182 | $method = new Method($accessLevel, $methodName, $hints, $fn); 183 | $method->setStatic(true); 184 | $this->methods[] = $method; 185 | } 186 | 187 | /** 188 | * @param string The name of the class to be created. 189 | * @return void 190 | * @throws InvalidArgumentException if the class has already been used by another class, interface or trait 191 | */ 192 | public function setName($className) 193 | { 194 | if (class_exists($className) || trait_exists($className) || interface_exists($className)) { 195 | throw new InvalidArgumentException("The name $className has already been taken by something else"); 196 | } 197 | $this->className = $className; 198 | } 199 | 200 | /** 201 | * @param string $className T 202 | * @return void 203 | * @throws InvalidArgumentException if the class to extend does not exist 204 | */ 205 | public function setExtends($className) 206 | { 207 | if (!class_exists($className)) { 208 | throw new \InvalidArgumentException("$className is not a valid class and cannot be extended"); 209 | } 210 | $this->extends = " extends $className "; 211 | } 212 | 213 | /** 214 | * @param string $interfaceName 215 | * @return void 216 | */ 217 | public function addInterface($interfaceName) 218 | { 219 | if (!interface_exists($interfaceName)) { 220 | throw new \InvalidArgumentException("$interfaceName is not a valid interface and cannot be implemented"); 221 | } 222 | $this->interfaces[] = $interfaceName; 223 | } 224 | 225 | /** 226 | * @param string $traitName 227 | * @return void 228 | */ 229 | public function addTrait($traitName) 230 | { 231 | if (!trait_exists($traitName)) { 232 | throw new \InvalidArgumentException("$traitName is not a valid trait and cannot be mixed in"); 233 | } 234 | $this->traits[] = "use $traitName;"; 235 | } 236 | 237 | /** 238 | * Helper method to disable the constructor of the parent class. This 239 | * just creates a no-op constructor in our constructed class. 240 | * @return void 241 | */ 242 | public function disableConstructor() 243 | { 244 | $this->constructor = "public function __construct() { }"; 245 | 246 | /** 247 | * NOTE: we set the name of class here to maintain parity with the side-effects of a dirty 248 | * hack in the PHPUnit Mock builder, where it made a fake serialized class and unserialized 249 | * it, which populates a "class" instance variable with the name of the class 250 | */ 251 | $this->constructor .= "\n\npublic \$class = '".$this->className."';\n"; 252 | } 253 | } 254 | 255 | /** 256 | * @package ClassBuilder 257 | * Defines a method that can be built 258 | */ 259 | class Method 260 | { 261 | private $accessLevel; 262 | private $methodName; 263 | private $argList; 264 | private $callable; 265 | private $thisCallback; 266 | private $isStatic = false; 267 | 268 | /** 269 | * @param string $accessLevel must be public, protected or private 270 | * @param string $methodName must be [a-zA-Z\_][a-zA-Z\_\d]* and unique to the class 271 | * @param string[] $typeList describes the arguments defined on the method signature 272 | * @param callable $callable the implementation of the method 273 | */ 274 | public function __construct($accessLevel, $methodName, $typeList, $callable) 275 | { 276 | if (!in_array($accessLevel, ["private", "protected", "public"])) { 277 | throw new InvalidArgumentException("$accessLevel is not a valid level"); 278 | } 279 | $this->accessLevel = $accessLevel; 280 | $this->methodName = $methodName; 281 | $this->typeList = $typeList; 282 | $this->callable = $callable; 283 | } 284 | 285 | /** 286 | * @return string the rendered method. Once eval'ing this method, you must 287 | * call Method->addToBuiltClass($clazz) on the eval'd class to register the 288 | * implementation. 289 | */ 290 | public function render() 291 | { 292 | $functionTemplate = << function () 298 | { 299 | \$fn = self::\$__implementations__[""]; 300 | 301 | \$joinPoint = new \Shmock\ClassBuilder\DecoratorJoinPoint(,"",\$fn); 302 | \$joinPoint->setArguments(func_get_args()); 303 | \$joinPoint->setDecorators(self::\$__decorators__); 304 | 305 | return \$joinPoint->execute(); 306 | } 307 | 308 | EOF; 309 | $engine = new SprintfEngine("<", ">"); 310 | 311 | return $engine->render($functionTemplate, [ 312 | "accessLevel" => $this->accessLevel, 313 | "methodName" => $this->methodName, 314 | "typeList" => implode($this->typeList, ","), 315 | "static" => $this->isStatic ? "static" : "", 316 | "execTarget" => $this->isStatic ? "get_called_class()" : "\$this", 317 | ]); 318 | } 319 | 320 | /** 321 | * Register the underlying behavior of this function on the target class. 322 | * @param string the class 323 | * @return void 324 | */ 325 | public function addToBuiltClass($class) 326 | { 327 | $class::__add_implementation__($this->methodName, $this->callable); 328 | } 329 | 330 | /** 331 | * @param bool 332 | * @return void 333 | */ 334 | public function setStatic($isStatic) 335 | { 336 | $this->isStatic = $isStatic; 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /test/Shmock/StaticMockTest.php: -------------------------------------------------------------------------------- 1 | staticClass = new PHPUnitStaticClass($this, $clazz); 20 | 21 | return $this->staticClass; 22 | } 23 | 24 | protected function getClassBuilderStaticClass($clazz) 25 | { 26 | $this->staticClass = new ClassBuilderStaticClass($this, $clazz); 27 | 28 | return $this->staticClass; 29 | 30 | } 31 | 32 | /** 33 | * @return callable these callables return instances of Instance that specialize 34 | * on returning mocked static classes. 35 | */ 36 | public function instanceProviders() 37 | { 38 | return [ 39 | // [[$this, "getPHPUnitStaticClass"]], // requires process isolation 40 | [[$this, "getClassBuilderStaticClass"]] 41 | ]; 42 | } 43 | 44 | private function buildMockClass(callable $getClass, callable $setup) 45 | { 46 | $this->staticClass = $getClass("\Shmock\ClassToMockStatically"); 47 | $setup($this->staticClass); 48 | 49 | return $this->staticClass->replay(); 50 | } 51 | 52 | /** 53 | * @dataProvider instanceProviders 54 | */ 55 | public function testMockClassesCanExpectMethodsBeInvoked(callable $getClass) 56 | { 57 | $mock = $this->buildMockClass($getClass, function ($staticClass) { 58 | $staticClass->getAnInt()->return_value(5); 59 | }); 60 | 61 | $this->assertEquals(5, $mock::getAnInt()); 62 | } 63 | 64 | /** 65 | * @dataProvider instanceProviders 66 | */ 67 | public function testFrequenciesCanBeEnforced(callable $getClass) 68 | { 69 | $mock = $this->buildMockClass($getClass, function ($staticClass) { 70 | $staticClass->getAnInt()->times(2)->return_value(10); 71 | }); 72 | 73 | $this->assertEquals(10, $mock::getAnInt()); 74 | $this->assertEquals(10, $mock::getAnInt()); 75 | 76 | $this->assertFailsMockExpectations(function () use ($mock) { 77 | $mock::getAnInt(); 78 | }, "the third invocation of getAnInt should have triggered the frequency check"); 79 | 80 | } 81 | 82 | /** 83 | * @dataProvider instanceProviders 84 | */ 85 | public function testMockClassesCanHaveAnyNumberOfInvocations(callable $getClass) 86 | { 87 | $mock = $this->buildMockClass($getClass, function ($staticClass) { 88 | $staticClass->getAnInt()->any()->return_value(15); 89 | }); 90 | 91 | // should not fail here 92 | } 93 | 94 | /** 95 | * @dataProvider instanceProviders 96 | */ 97 | public function testMockClassesCanHaveExactlyZeroInvocations(callable $getClass) 98 | { 99 | $mock = $this->buildMockClass($getClass, function ($staticClass) { 100 | $staticClass->getAnInt()->never(); 101 | }); 102 | 103 | $this->assertFailsMockExpectations(function () use ($mock) { 104 | $mock::getAnInt(); 105 | }, "expected to never invoke the mock object"); 106 | } 107 | 108 | /** 109 | * @dataProvider instanceProviders 110 | */ 111 | public function testAtLeastOnceAllowsManyInvocations(callable $getClass) 112 | { 113 | $mock = $this->buildMockClass($getClass, function ($staticClass) { 114 | $staticClass->getAnInt()->at_least_once()->return_value(15); 115 | }); 116 | 117 | $mock::getAnInt(); 118 | $mock::getAnInt(); 119 | } 120 | 121 | /** 122 | * @dataProvider instanceProviders 123 | */ 124 | public function testAtLeastOnceErrsWhenZeroInvocations(callable $getClass) 125 | { 126 | $this->buildMockClass($getClass, function ($staticClass) { 127 | $staticClass->getAnInt()->at_least_once()->return_value(15); 128 | }); 129 | 130 | $this->assertMockObjectsShouldFail("at least once should fail when there are no invocations"); 131 | } 132 | 133 | /** 134 | * @dataProvider instanceProviders 135 | */ 136 | public function testNoExplicitFrequencyIsImpliedOnce(callable $getClass) 137 | { 138 | $this->buildMockClass($getClass, function ($staticClass) { 139 | $staticClass->getAnInt()->return_value(4); 140 | }); 141 | 142 | $this->assertMockObjectsShouldFail("implied once should fail when there are no invocations"); 143 | } 144 | 145 | /** 146 | * @dataProvider instanceProviders 147 | */ 148 | public function testPassingCallableToWillCausesInvocationWhenMockIsUsed(callable $getClass) 149 | { 150 | $mock = $this->buildMockClass($getClass, function ($staticClass) { 151 | $staticClass->getAnInt()->will(function ($i) { return 999 + $i->parameters[0]; }); 152 | }); 153 | 154 | $this->assertEquals(1000, $mock::getAnInt(1)); 155 | } 156 | 157 | /** 158 | * @dataProvider instanceProviders 159 | */ 160 | public function testReturnValueMapWillRespondWithLastValuesInArrayGivenTheArguments(callable $getClass) 161 | { 162 | $mock = $this->buildMockClass($getClass, function ($staticClass) { 163 | $staticClass->multiply()->return_value_map([ 164 | [10, 20, 30], 165 | [1, 2, 3], 166 | [["a" => "b"], ["b" => "c"], 1] 167 | ])->any(); 168 | }); 169 | 170 | $this->assertEquals(30, $mock::multiply(10, 20)); 171 | $this->assertSame(1, $mock::multiply(["a"=> "b"], ["b" => "c"])); 172 | 173 | $this->assertFailsMockExpectations(function () use ($mock) { 174 | $mock::multiply(10, 2); 175 | }, "Expected no match on the passed arguments"); 176 | } 177 | 178 | /** 179 | * @dataProvider instanceProviders 180 | */ 181 | public function testReturnThisWillReturnTheClassItself(callable $getClass) 182 | { 183 | $mock = $this->buildMockClass($getClass, function ($staticClass) { 184 | $staticClass->getAnInt()->return_this(); 185 | }); 186 | 187 | $this->assertSame($mock, $mock::getAnInt()); 188 | } 189 | 190 | /** 191 | * @dataProvider instanceProviders 192 | */ 193 | public function testThrowExceptionWillTriggerAnExceptionOnUse(callable $getClass) 194 | { 195 | $mock = $this->buildMockClass($getClass, function ($staticClass) { 196 | $staticClass->getAnInt()->throw_exception(new \LogicException()); 197 | }); 198 | 199 | try { 200 | $mock::getAnInt(); 201 | $this->fail("There should have been a logic exception thrown"); 202 | } catch (\LogicException $e) { 203 | 204 | } 205 | } 206 | 207 | /** 208 | * @dataProvider instanceProviders 209 | */ 210 | public function testThrowExceptionWillUseADefaultExceptionTypeIfNonePassed(callable $getClass) 211 | { 212 | $mock = $this->buildMockClass($getClass, function ($staticClass) { 213 | $staticClass->getAnInt()->throw_exception(); 214 | }); 215 | 216 | try { 217 | $mock::getAnInt(); 218 | $this->fail("There should have been a logic exception thrown"); 219 | } catch (\Exception $e) { 220 | if (preg_match('/PHPUnit.*/', get_class($e))) { 221 | throw $e; 222 | } 223 | } 224 | } 225 | 226 | /** 227 | * @dataProvider instanceProviders 228 | */ 229 | public function testReturnConsecutivelyReturnsValuesInASequence(callable $getClass) 230 | { 231 | $mock = $this->buildMockClass($getClass, function ($staticClass) { 232 | $staticClass->getAnInt()->return_consecutively([1,2,3]); 233 | }); 234 | 235 | $this->assertEquals(1, $mock::getAnInt()); 236 | $this->assertEquals(2, $mock::getAnInt()); 237 | $this->assertEquals(3, $mock::getAnInt()); 238 | } 239 | 240 | /** 241 | * @dataProvider instanceProviders 242 | */ 243 | public function testReturnShmockOpensNestedMockingFacility(callable $getClass) 244 | { 245 | $mock = $this->buildMockClass($getClass, function ($staticClass) { 246 | $staticClass->getAnInt()->return_shmock('\Shmock\ClassToMockStatically', function ($toMock) { 247 | // unimportant 248 | }); 249 | }); 250 | 251 | $nestedMock = $mock::getAnInt(); 252 | $this->assertTrue(is_a($nestedMock,'\Shmock\ClassToMockStatically')); 253 | } 254 | 255 | /** 256 | * @dataProvider instanceProviders 257 | */ 258 | public function testUnmockedFunctionsRemainIntact(callable $getClass) 259 | { 260 | $mock = $this->buildMockClass($getClass, function ($staticClass) { 261 | }); 262 | 263 | $this->assertSame(1, $mock::getAnInt()); 264 | } 265 | 266 | /** 267 | * @dataProvider instanceProviders 268 | */ 269 | public function testUnmockedFunctionsElidedIfPreservationDisabled($getClass) 270 | { 271 | $mock = $this->buildMockClass($getClass, function ($staticClass) { 272 | $staticClass->dont_preserve_original_methods(); 273 | }); 274 | 275 | $this->assertNull($mock::getAnInt()); 276 | } 277 | 278 | /** 279 | * @dataProvider instanceProviders 280 | */ 281 | public function testOrderMattersWillEnforceCorrectOrderingOfCalls($getClass) 282 | { 283 | $mock = $this->buildMockClass($getClass, function ($staticClass) { 284 | $staticClass->order_matters(); 285 | $staticClass->getAnInt()->return_value(2); 286 | $staticClass->getAnInt()->return_value(4); 287 | }); 288 | 289 | $this->assertSame(2, $mock::getAnInt()); 290 | $this->assertSame(4, $mock::getAnInt()); 291 | } 292 | 293 | /** 294 | * @dataProvider instanceProviders 295 | */ 296 | public function testOrderMattersWillPreventOutOfOrderCalls($getClass) 297 | { 298 | $mock = $this->buildMockClass($getClass, function ($staticClass) { 299 | $staticClass->order_matters(); 300 | $staticClass->getAnInt()->return_value(2); 301 | $staticClass->multiply(2, 2)->return_value(4); 302 | }); 303 | 304 | $this->assertFailsMockExpectations(function () use ($mock) { 305 | $mock::multiply(2, 2); 306 | }, "Expected the multiply call to be out of order"); 307 | } 308 | 309 | /** 310 | * @dataProvider instanceProviders 311 | */ 312 | public function testArgumentsShouldBeEnforced($getClass) 313 | { 314 | $mock = $this->buildMockClass($getClass, function ($staticClass) { 315 | $staticClass->multiply(2,2)->return_value(4); 316 | }); 317 | 318 | $this->assertFailsMockExpectations(function () use ($mock) { 319 | $mock::multiply(2,3); 320 | }, "Expected the multiply call to err due to bad args"); 321 | } 322 | 323 | /** 324 | * @dataProvider instanceProviders 325 | */ 326 | public function testArrayArgumentsShouldBeEnforced($getClass) 327 | { 328 | $mock = $this->buildMockClass($getClass, function ($staticClass) { 329 | $staticClass->multiply([2,2],2)->any()->return_value([4,4]); 330 | }); 331 | 332 | $this->assertSame([4,4], $mock::multiply([2,2],2)); 333 | 334 | $this->assertFailsMockExpectations(function () use ($mock) { 335 | $mock::multiply([2,3],3); 336 | }, "Expected the multiply call to err due to bad args"); 337 | } 338 | 339 | /** 340 | * @dataProvider instanceProviders 341 | */ 342 | public function testPHPUnitConstraintsAllowed($getClass) 343 | { 344 | $mock = $this->buildMockClass($getClass, function ($staticClass) { 345 | $staticClass->multiply($this->isType("integer"), $this->greaterThan(2))->any()->return_value(10); 346 | }); 347 | 348 | // just bare with me here... 349 | $this->assertSame(10, $mock::multiply(2, 4)); 350 | $this->assertSame(10, $mock::multiply(10, 5)); 351 | 352 | $this->assertFailsMockExpectations(function () use ($mock) { 353 | $mock::multiply(2.0, 1); 354 | }, "expected the underlying constraints to fail"); 355 | } 356 | 357 | /** 358 | * @dataProvider instanceProviders 359 | */ 360 | public function testShmockCanSubclassFunctionsWithReferenceArgs($getClass) 361 | { 362 | $a = 5; 363 | $mock = $this->buildMockClass($getClass, function ($staticClass) { 364 | $staticClass->reference(5)->will(function ($inv) { 365 | return $inv->parameters[0] + 1; 366 | }); 367 | }); 368 | 369 | $a = $mock::reference($a); 370 | $this->assertSame(6, $a, "expected shmock to preserve reference semantics"); 371 | } 372 | 373 | /** 374 | * @dataProvider instanceProviders 375 | */ 376 | public function testJuggledTypesAreConsideredMatches($getClass) 377 | { 378 | $mock = $this->buildMockClass($getClass, function ($staticClass) { 379 | $staticClass->multiply("1", "2")->return_value(2); 380 | }); 381 | 382 | $this->assertSame(2, $mock::multiply(1, 2.0)); 383 | } 384 | 385 | /** 386 | * @dataProvider instanceProviders 387 | */ 388 | public function testExtraArgumentsWithNoExplicitConstraintAreIgnored($getClass) 389 | { 390 | $mock = $this->buildMockClass($getClass, function ($staticClass) { 391 | $staticClass->multiply(1,2)->return_value(2); 392 | }); 393 | 394 | $this->assertSame(2, $mock::multiply(1,2,3)); 395 | } 396 | } 397 | 398 | class ClassToMockStatically 399 | { 400 | private $a; 401 | 402 | public function __construct($a = null) 403 | { 404 | $this->a = $a; 405 | } 406 | 407 | public function getA() 408 | { 409 | return $this->a; 410 | } 411 | 412 | public static function getAnInt() 413 | { 414 | return 1; 415 | } 416 | 417 | public static function multiply($a, $b) 418 | { 419 | return $a * $b; 420 | } 421 | 422 | public static function reference(& $a) 423 | { 424 | $a++; 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /src/Shmock/StaticSpec.php: -------------------------------------------------------------------------------- 1 | testCase = $testCase; 68 | $this->className = $className; 69 | $this->methodName = $methodName; 70 | $this->arguments = $arguments; 71 | $this->frequency = new CountOfTimes(1, $this->methodName); 72 | $this->policies = $policies; 73 | 74 | if ($this->shouldDoStrictMethodCheck()) { 75 | $this->doStrictMethodCheck(); 76 | } 77 | 78 | if ($this->shouldCheckArgsAgainstPolicies()) { 79 | foreach ($this->policies as $policy) { 80 | $policy->check_method_parameters($className, $methodName, $arguments, $this->isStatic()); 81 | } 82 | } 83 | } 84 | 85 | /** 86 | * This is a backwards compatability escape valve to prevent strict method checks when 87 | * a user requests it for static methods. See the comment in the Shmock class for more context 88 | * @return bool 89 | */ 90 | private function shouldDoStrictMethodCheck() 91 | { 92 | return !($this->isStatic() && Shmock::$disable_strict_method_checks_for_static_methods); 93 | } 94 | 95 | /** 96 | * @return bool 97 | */ 98 | protected function shouldCheckArgsAgainstPolicies() 99 | { 100 | return true; 101 | } 102 | 103 | /** 104 | * @return bool 105 | */ 106 | protected function isStatic() 107 | { 108 | return true; 109 | } 110 | 111 | /** 112 | * @return void 113 | */ 114 | protected function doStrictMethodCheck() 115 | { 116 | if ($this->isStatic()) { 117 | $errMsg = "#{$this->methodName} is an instance method on the class {$this->className}, but you expected it to be static."; 118 | } else { 119 | $errMsg = "#{$this->methodName} is a static method on the class {$this->className}, but you expected it to be an instance method."; 120 | } 121 | 122 | try { 123 | $reflectionMethod = new \ReflectionMethod($this->className, $this->methodName); 124 | $this->testCase->assertTrue($reflectionMethod->isStatic() == $this->isStatic(), $errMsg); 125 | $this->testCase->assertFalse($reflectionMethod->isPrivate(), "#{$this->methodName} is a private method on {$this->className}, but you cannot mock a private method."); 126 | } catch (\ReflectionException $e) { 127 | $necessaryMagicMethod = $this->isStatic() ? '__callStatic' : '__call'; 128 | $this->testCase->assertTrue(method_exists($this->className, $necessaryMagicMethod), "The method #{$this->methodName} does not exist on the class {$this->className}"); 129 | } 130 | } 131 | 132 | /** 133 | * Specify that the method will be invoked $times times. 134 | *
135 |      *  // expect notify will be called 5 times
136 |      *  $shmock->notify()->times(5);
137 |      * 
138 | * 139 | * @param int|null $times the number of times to expect the given call 140 | * @return \Shmock\Spec 141 | * @see \Shmock\Spec::at_least_once() See at_least_once() 142 | */ 143 | public function times($times) 144 | { 145 | $this->frequency = new CountOfTimes($times, $this->methodName); 146 | 147 | return $this; 148 | } 149 | 150 | /** 151 | * Specify that the method will be invoked once. 152 | * 153 | * This is a shorthand for times(1) 154 | * @return \Shmock\Spec 155 | * @see \Shmock\Spec::times() See times() 156 | */ 157 | public function once() 158 | { 159 | $this->frequency = new CountOfTimes(1, $this->methodName); 160 | 161 | return $this; 162 | } 163 | 164 | /** 165 | * Specify that the method will be invoked twice. 166 | * 167 | * This is a shorthand for times(2) 168 | * @return \Shmock\Spec 169 | * @see \Shmock\Spec::times() See times() 170 | */ 171 | public function twice() 172 | { 173 | $this->frequency = new CountOfTimes(2, $this->methodName); 174 | 175 | return $this; 176 | } 177 | 178 | /** 179 | * Specifies that the number of invocations of this method 180 | * is not to be verified by Shmock. 181 | *
182 |      *  $shmock->notify()->any();
183 |      * 
184 | * This is a shorthand for times(null) 185 | * @return \Shmock\Spec 186 | * @see \Shmock\Spec::times() See times() 187 | */ 188 | public function any() 189 | { 190 | $this->frequency = new AnyTimes(); 191 | 192 | return $this; 193 | } 194 | 195 | /** 196 | * Specifies that this method is never to be invoked. 197 | * 198 | * This is an alias for times(0) 199 | * @return \Shmock\Spec 200 | * @see \Shmock\Spec::times() See times() 201 | */ 202 | public function never() 203 | { 204 | $this->frequency = new CountOfTimes(0, $this->methodName); 205 | 206 | return $this; 207 | } 208 | 209 | /** 210 | * Specifies that the method is to be invoked at least once 211 | * but possibly more. 212 | * This directive is only respected if no other calls to times() have been recorded. 213 | *
214 |      *  $shmock->notify()->at_least_once();
215 |      * 
216 | * 217 | * @return \Shmock\Spec 218 | * @see \Shmock\Spec::times() See times() 219 | */ 220 | public function at_least_once() 221 | { 222 | $this->frequency = new AtLeastOnce($this->methodName); 223 | 224 | return $this; 225 | } 226 | 227 | /** 228 | * Specifies that the given closure will be executed on invocation. 229 | * The first argument to the closure is an instance of \PHPUnit_Framework_MockObject_Invocation. 230 | * 231 | *
232 |      *  // custom action with a closure
233 |      *
234 |      *  $shmock->notify()->will(function ($invocation) {
235 |      *    $this->assertTrue(count($invocation->parameters) > 2);
236 |      *  });
237 |      * 
238 | * 239 | * @param callable $will_closure 240 | * @return \Shmock\Spec 241 | */ 242 | public function will($will_closure) 243 | { 244 | $this->will = $will_closure; 245 | 246 | return $this; 247 | } 248 | 249 | /** 250 | * An order-agnostic set of return values given a set of inputs. 251 | * 252 | * @param mixed[][] $map_of_args_to_values an array of arrays of arguments with the final value 253 | * of the array being the return value. 254 | * @return \Shmock\Spec 255 | * For example, if you were simulating addition: 256 | *
257 |     * $shmock_calculator->add()->return_value_map([
258 |     * 	[1, 2, 3], // 1 + 2 = 3
259 |     * 	[10, 15, 25],
260 |     * 	[11, 11, 22]
261 |     * ]);
262 |     * 
263 | */ 264 | public function return_value_map($mapOfArgsToValues) 265 | { 266 | if (count($mapOfArgsToValues) < 1) { 267 | throw new \InvalidArgumentException('Must specify at least one return value'); 268 | }; 269 | 270 | $limit = count($mapOfArgsToValues); 271 | 272 | $this->frequency = new CountOfTimes($limit, $this->methodName); 273 | 274 | /* 275 | * make the mapping a little more sane: if we received 276 | * [ 277 | * [1,2,3], 278 | * [4,5,9] 279 | * ] 280 | * as a map, convert it to: 281 | * [ 282 | * [[1,2], 3], 283 | * [[4,5], 9], 284 | * ] 285 | */ 286 | $mapping = []; 287 | foreach ($mapOfArgsToValues as $paramsAndReturn) { 288 | $parameterSet = array_slice($paramsAndReturn, 0, count($paramsAndReturn) - 1); 289 | $returnVal = $paramsAndReturn[count($paramsAndReturn) - 1]; 290 | $mapping[] = [$parameterSet, $returnVal]; 291 | } 292 | 293 | foreach ($this->policies as $policy) { 294 | foreach ($mapping as $paramsWithReturn) { 295 | $policy->check_method_parameters($this->className, $this->methodName, $paramsWithReturn[0], $this->isStatic()); 296 | $policy->check_method_return_value($this->className, $this->methodName, $paramsWithReturn[1], $this->isStatic()); 297 | } 298 | } 299 | 300 | $this->will = function ($invocation) use ($mapping) { 301 | $args = $invocation->parameters; 302 | foreach ($mapping as $map) { 303 | list($possibleArgs, $possibleRet) = $map; 304 | if ($possibleArgs === $args) { 305 | return $possibleRet; 306 | } 307 | } 308 | 309 | $differ = new \SebastianBergmann\Diff\Differ(); 310 | $diffSoFar = null; 311 | foreach ($mapping as $map) { 312 | $possibleArgs = $map[0]; 313 | $nextDiff = $differ->diff(print_r($possibleArgs, true), print_r($args, true)); 314 | if ($diffSoFar === null || strlen($nextDiff) < strlen($diffSoFar)) { 315 | $diffSoFar = $nextDiff; 316 | } 317 | } 318 | throw new \PHPUnit_Framework_AssertionFailedError(sprintf("Did not expect to be called with args %s, diff with closest match is\n%s", print_r($args, true), $diffSoFar)); 319 | }; 320 | 321 | return $this; 322 | } 323 | 324 | /** 325 | * Specifies that the method will return true. 326 | * This is a shorthand for return_value(true) 327 | * @return \Shmock\Spec 328 | */ 329 | public function return_true() 330 | { 331 | return $this->return_value(true); 332 | } 333 | 334 | /** 335 | * Specifies that the method will return false. 336 | * This is a shorthand for return_value(false) 337 | * @return \Shmock\Spec 338 | */ 339 | public function return_false() 340 | { 341 | return $this->return_value(false); 342 | } 343 | 344 | /** 345 | * Specifies that the method will return null. 346 | * This is a shorthand for return_value(null) 347 | * @return \Shmock\Spec 348 | */ 349 | public function return_null() 350 | { 351 | return $this->return_value(null); 352 | } 353 | 354 | /** 355 | * Specifies that the method will return the given value on invocation. 356 | *
357 |      *  $shmock->notify()->return_value("notification!");
358 |      * 
359 | * @param mixed|null $value The value to return on invocation 360 | * @return \Shmock\Spec 361 | * @see \Shmock\Instance::order_matters() If you wish to specify multiple return values and the order is important, look at Instance::order_matters() 362 | * @see \Shmock\Spec::return_value_map() If you wish to specify multiple return values contingent on the parameters, but otherwise insensitive to the order, look at return_value_map() 363 | */ 364 | public function return_value($value) 365 | { 366 | $this->returnValue = $value; 367 | foreach ($this->policies as $policy) { 368 | $policy->check_method_return_value($this->className, $this->methodName, $value, $this->isStatic()); 369 | } 370 | 371 | return $this; 372 | } 373 | 374 | /** 375 | * Specifies that the method will return the invocation target. This is 376 | * useful for mocking other objects that have fluent interfaces. 377 | *
378 |      *  $latte->add_foam()->return_this();
379 |      *  $latte->caffeine_free()->return_this();
380 |      * 
381 | * @return \Shmock\Spec 382 | */ 383 | public function return_this() 384 | { 385 | $this->returnThis = true; 386 | 387 | return $this; 388 | } 389 | 390 | /** 391 | * Throws an exception on invocation. 392 | * @param \Exception|void $e the exception to throw. If not specified, Shmock will provide an instance of 393 | * the base \Exception. 394 | * @return \Shmock\Spec 395 | */ 396 | public function throw_exception($e=null) 397 | { 398 | $e = $e ?: new \Exception(); 399 | 400 | foreach ($this->policies as $policy) { 401 | $policy->check_method_throws($this->className, $this->methodName, $e, true); 402 | } 403 | 404 | $this->will(function () use ($e) { 405 | throw $e; 406 | }); 407 | 408 | return $this; 409 | } 410 | 411 | /** 412 | * Specifies that each subsequent invocation of this method will take the subsequent value from the array as the return value. 413 | * 414 | * The sequence of values to return is not affected by ordering constraints on the mock (ie, order_matters()). 415 | * 416 | *
417 |      *  $shmock->notify()->return_consecutively(["called!", "called again!", "called a third time!"]);
418 |      *
419 |      *  $mock = $shmock->replay(); // replay is automatically called at the end of \Shmock\Shmock::create()
420 |      *
421 |      *  $mock->notify(); // called!
422 |      *  $mock->notify(); // called again!
423 |      *  $mock->notify(); // called a third time!
424 |      * 
425 | * @param mixed[] $array_of_values the sequence of values to return. 426 | * @param boolean|void $keep_returning_last_value whether to continue returning the last element in the sequence 427 | * or to fail the count expectation after every sequence element has been used. Defaults to false. 428 | * @return \Shmock\Spec 429 | */ 430 | public function return_consecutively($array_of_values, $keep_returning_last_value=false) 431 | { 432 | foreach ($this->policies as $policy) { 433 | foreach ($array_of_values as $value) { 434 | $policy->check_method_return_value($this->className, $this->methodName, $value, true); 435 | } 436 | } 437 | 438 | // $this->returned_values = array_merge($this->returned_values, $array_of_values); 439 | $this->will = function () use ($array_of_values, $keep_returning_last_value) { 440 | static $counter = -1; 441 | $counter++; 442 | if ($counter == count($array_of_values)) { 443 | if ($keep_returning_last_value) { 444 | return $array_of_values[count($array_of_values)-1]; 445 | } 446 | } else { 447 | return $array_of_values[$counter]; 448 | } 449 | }; 450 | if (!$keep_returning_last_value) { 451 | $this->times(count($array_of_values)); 452 | } 453 | 454 | return $this; 455 | } 456 | 457 | /** 458 | * Specifies that the return value from this function will be a new mock object, which is 459 | * built and replayed as soon as the invocation has occurred. 460 | * The signature of return_shmock() is similar to \Shmock\Shmock::create() 461 | * except that the test case argument is omitted 462 | * 463 | *
464 |      *  $user = \Shmock\Shmock::create($this, 'User', function ($user) {
465 |      *    $user->supervisor()->return_shmock('Supervisor', function ($supervisor) {
466 |      *      $supervisor->send_angry_email("I need you to work this weekend");
467 |      *    });
468 |      *  });
469 |      * 
470 | * @param string $class the name of the class to mock. 471 | * @param callable $shmock_closure a closure that will act as the class's build phase. 472 | * @return \Shmock\Spec 473 | */ 474 | public function return_shmock($class, $shmockClosure) 475 | { 476 | $phpunitInstance = new ClassBuilderInstanceClass($this->testCase, $class); 477 | $shmockClosure($phpunitInstance); 478 | 479 | return $this->return_value($phpunitInstance->replay()); 480 | } 481 | 482 | /** 483 | * @internal invoked at the end of the build() phase 484 | * @param mixed $mock 485 | * @param \Shmock\Policy[] $policies 486 | * @param boolean $static 487 | * @param string the name of the class being mocked 488 | * @return void 489 | */ 490 | public function __shmock_finalize_expectations($mock, array $policies, $static, $class) 491 | { 492 | 493 | } 494 | 495 | /** 496 | * @return string the name of the specification 497 | */ 498 | public function __shmock_name() 499 | { 500 | return "Expectations for {$this->methodName}"; 501 | } 502 | 503 | /** 504 | * @return void 505 | */ 506 | public function __shmock_verify() 507 | { 508 | $this->frequency->verify(); 509 | } 510 | 511 | /** 512 | * @param mixed|null 513 | * @param mixed|null 514 | * @return bool 515 | */ 516 | private function argumentMatches($expected, $actual) 517 | { 518 | if (is_a($expected, '\PHPUnit_Framework_Constraint')) { 519 | return $expected->evaluate($actual,"", true); 520 | } else { 521 | return $expected == $actual; 522 | } 523 | } 524 | 525 | /** 526 | * @param \Shmock\ClassBuilder\Invocation 527 | * @return mixed|null 528 | */ 529 | public function doInvocation(Invocation $invocation) 530 | { 531 | $this->frequency->addCall(); 532 | 533 | $args = $invocation->getArguments(); 534 | 535 | $i = 0; 536 | foreach ($this->arguments as $expected) { 537 | $argi = null; 538 | if ($i < count($args)) { 539 | $argi = $args[$i]; 540 | } 541 | if (!$this->argumentMatches($expected, $argi)) { 542 | $expectedStr = print_r($expected, true); 543 | $actualStr = print_r($argi, true); 544 | $extra = ""; 545 | if (strlen($expectedStr) > 100) { 546 | $differ = new \SebastianBergmann\Diff\Differ(); 547 | $extra = "Diff: \n" . $differ->diff($expectedStr, $actualStr); 548 | } 549 | throw new \PHPUnit_Framework_AssertionFailedError(sprintf("Unexpected argument#%s %s (%s) to method '%s', was expecting %s (%s). %s", $i, $actualStr, gettype($argi), $this->methodName, $expectedStr, print_r(gettype($expected), true), $extra)); 550 | } 551 | 552 | $i++; 553 | } 554 | 555 | if ($this->will) { 556 | return call_user_func($this->will, $invocation); 557 | } 558 | 559 | if ($this->returnThis) { 560 | $target = $invocation->getTarget(); 561 | 562 | // as implemented, returnThis can only be verified by policies at 563 | // calltime. 564 | foreach ($this->policies as $policy) { 565 | $policy->check_method_return_value($this->className, $this->methodName, $target, true); 566 | } 567 | 568 | return $target; 569 | } 570 | 571 | return $this->returnValue; 572 | } 573 | } 574 | --------------------------------------------------------------------------------