├── .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 | * - The class being mocked doesn't exist.
191 | * - The method being mocked doesn't exist AND there is no __call handler on the class.
192 | * - The method is private.
193 | * - The method is static. (or non-static if using a StaticClass )
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 |
--------------------------------------------------------------------------------