diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f9ba81e5..7142ae679 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 0.9.3 (XXXX-XX-XX) + +* Added a basic spy implementation + ## 0.9.2 (2014-09-03) * Some workarounds for the serilisation problems created by changes to PHP in 5.5.13, 5.4.29, diff --git a/library/Mockery.php b/library/Mockery.php index b16d9ec52..1f1e2c862 100644 --- a/library/Mockery.php +++ b/library/Mockery.php @@ -75,8 +75,15 @@ public static function mock() } /** - * Another shortcut to \Mockery\Container:mock(). - * + * @return \Mockery\MockInterface + */ + public static function spy() + { + $args = func_get_args(); + return call_user_func_array(array(self::getContainer(), 'mock'), $args)->shouldIgnoreMissing(); + } + + /** * @return \Mockery\MockInterface */ public static function instanceMock() diff --git a/library/Mockery/Expectation.php b/library/Mockery/Expectation.php index 915cae754..19a1573c4 100644 --- a/library/Mockery/Expectation.php +++ b/library/Mockery/Expectation.php @@ -693,4 +693,9 @@ public function __clone() $this->_countValidators = $newValidators; } + public function getName() + { + return $this->_name; + } + } diff --git a/library/Mockery/Generator/StringManipulation/Pass/MethodDefinitionPass.php b/library/Mockery/Generator/StringManipulation/Pass/MethodDefinitionPass.php index 6a714990a..d0f45317e 100644 --- a/library/Mockery/Generator/StringManipulation/Pass/MethodDefinitionPass.php +++ b/library/Mockery/Generator/StringManipulation/Pass/MethodDefinitionPass.php @@ -73,7 +73,7 @@ protected function appendToClass($class, $code) private function renderMethodBody($method, $config) { - $invoke = $method->isStatic() ? 'static::__callStatic' : '$this->__call'; + $invoke = $method->isStatic() ? 'static::_mockery_handleStaticMethodCall' : '$this->_mockery_handleMethodCall'; $body = <<method = $method; + $this->args = $args; + } + + public function getMethod() + { + return $this->method; + } + + public function getArgs() + { + return $this->args; + } +} diff --git a/library/Mockery/Mock.php b/library/Mockery/Mock.php index 3ebc22835..1258b27a9 100644 --- a/library/Mockery/Mock.php +++ b/library/Mockery/Mock.php @@ -137,6 +137,8 @@ class Mock implements MockInterface protected $_mockery_allowMockingProtectedMethods = false; + protected $_mockery_receivedMethodCalls; + /** * If shouldIgnoreMissing is called, this value will be returned on all calls to missing methods * @var mixed @@ -312,68 +314,12 @@ public function byDefault() */ public function __call($method, array $args) { - $rm = $this->mockery_getMethod($method); - if ($rm && $rm->isProtected() && !$this->_mockery_allowMockingProtectedMethods) { - if ($rm->isAbstract()) { - return; - } - - try { - $prototype = $rm->getPrototype(); - if ($prototype->isAbstract()) { - return; - } - } catch (\ReflectionException $re) { - // noop - there is no hasPrototype method - } - - return call_user_func_array("parent::$method", $args); - } - - if (isset($this->_mockery_expectations[$method]) - && !$this->_mockery_disableExpectationMatching) { - $handler = $this->_mockery_expectations[$method]; - - try { - return $handler->call($args); - } catch (\Mockery\Exception\NoMatchingExpectationException $e) { - if (!$this->_mockery_ignoreMissing && !$this->_mockery_deferMissing) { - throw $e; - } - } - } - - if (!is_null($this->_mockery_partial) && method_exists($this->_mockery_partial, $method)) { - return call_user_func_array(array($this->_mockery_partial, $method), $args); - } elseif ($this->_mockery_deferMissing && is_callable("parent::$method")) { - return call_user_func_array("parent::$method", $args); - } elseif ($method == '__toString') { - // __toString is special because we force its addition to the class API regardless of the - // original implementation. Thus, we should always return a string rather than honor - // _mockery_ignoreMissing and break the API with an error. - return sprintf("%s#%s", __CLASS__, spl_object_hash($this)); - } elseif ($this->_mockery_ignoreMissing) { - if($this->_mockery_defaultReturnValue instanceof \Mockery\Undefined) - return call_user_func_array(array($this->_mockery_defaultReturnValue, $method), $args); - else - return $this->_mockery_defaultReturnValue; - } - throw new \BadMethodCallException( - 'Method ' . __CLASS__ . '::' . $method . '() does not exist on this mock object' - ); + return $this->_mockery_handleMethodCall($method, $args); } public static function __callStatic($method, array $args) { - try { - $associatedRealObject = \Mockery::fetchMock(__CLASS__); - return $associatedRealObject->__call($method, $args); - } catch (\BadMethodCallException $e) { - throw new \BadMethodCallException( - 'Static method ' . $associatedRealObject->mockery_getName() . '::' . $method - . '() does not exist on this mock object' - ); - } + return self::_mockery_handleStaticMethodCall($method, $args); } /** @@ -656,6 +602,103 @@ public function mockery_getMethod($name) return null; } + public function shouldHaveReceived($method, $args = null) + { + $expectation = new \Mockery\VerificationExpectation($this, $method); + if (null !== $args) { + $expectation->withArgs($args); + } + $expectation->atLeast()->once(); + $director = new \Mockery\VerificationDirector($this->_mockery_getReceivedMethodCalls(), $expectation); + $director->verify(); + return $director; + } + + public function shouldNotHaveReceived($method, $args = null) + { + $expectation = new \Mockery\VerificationExpectation($this, $method); + if (null !== $args) { + $expectation->withArgs($args); + } + $expectation->never(); + $director = new \Mockery\VerificationDirector($this->_mockery_getReceivedMethodCalls(), $expectation); + $director->verify(); + return $director; + } + + protected static function _mockery_handleStaticMethodCall($method, array $args) + { + try { + $associatedRealObject = \Mockery::fetchMock(__CLASS__); + return $associatedRealObject->__call($method, $args); + } catch (\BadMethodCallException $e) { + throw new \BadMethodCallException( + 'Static method ' . $associatedRealObject->mockery_getName() . '::' . $method + . '() does not exist on this mock object' + ); + } + } + + protected function _mockery_getReceivedMethodCalls() + { + return $this->_mockery_receivedMethodCalls ?: $this->_mockery_receivedMethodCalls = new \Mockery\ReceivedMethodCalls(); + } + + protected function _mockery_handleMethodCall($method, array $args) + { + $this->_mockery_getReceivedMethodCalls()->push(new \Mockery\MethodCall($method, $args)); + + $rm = $this->mockery_getMethod($method); + if ($rm && $rm->isProtected() && !$this->_mockery_allowMockingProtectedMethods) { + if ($rm->isAbstract()) { + return; + } + + try { + $prototype = $rm->getPrototype(); + if ($prototype->isAbstract()) { + return; + } + } catch (\ReflectionException $re) { + // noop - there is no hasPrototype method + } + + return call_user_func_array("parent::$method", $args); + } + + if (isset($this->_mockery_expectations[$method]) + && !$this->_mockery_disableExpectationMatching) { + $handler = $this->_mockery_expectations[$method]; + + try { + return $handler->call($args); + } catch (\Mockery\Exception\NoMatchingExpectationException $e) { + if (!$this->_mockery_ignoreMissing && !$this->_mockery_deferMissing) { + throw $e; + } + } + } + + if (!is_null($this->_mockery_partial) && method_exists($this->_mockery_partial, $method)) { + return call_user_func_array(array($this->_mockery_partial, $method), $args); + } elseif ($this->_mockery_deferMissing && is_callable("parent::$method")) { + return call_user_func_array("parent::$method", $args); + } elseif ($method == '__toString') { + // __toString is special because we force its addition to the class API regardless of the + // original implementation. Thus, we should always return a string rather than honor + // _mockery_ignoreMissing and break the API with an error. + return sprintf("%s#%s", __CLASS__, spl_object_hash($this)); + } elseif ($this->_mockery_ignoreMissing) { + if ($this->_mockery_defaultReturnValue instanceof \Mockery\Undefined) + return call_user_func_array(array($this->_mockery_defaultReturnValue, $method), $args); + else + return $this->_mockery_defaultReturnValue; + } + throw new \BadMethodCallException( + 'Method ' . __CLASS__ . '::' . $method . '() does not exist on this mock object' + ); + } + protected function mockery_getMethods() { if (static::$_mockery_methods) { diff --git a/library/Mockery/ReceivedMethodCalls.php b/library/Mockery/ReceivedMethodCalls.php new file mode 100644 index 000000000..d3b839d3a --- /dev/null +++ b/library/Mockery/ReceivedMethodCalls.php @@ -0,0 +1,30 @@ +methodCalls[] = $methodCall; + } + + public function verify(Expectation $expectation) + { + foreach ($this->methodCalls as $methodCall) { + if ($methodCall->getMethod() !== $expectation->getName()) { + continue; + } + + if (!$expectation->matchArgs($methodCall->getArgs())) { + continue; + } + + $expectation->verifyCall($methodCall->getArgs()); + } + + $expectation->verify(); + } +} diff --git a/library/Mockery/VerificationDirector.php b/library/Mockery/VerificationDirector.php new file mode 100644 index 000000000..a5e51126b --- /dev/null +++ b/library/Mockery/VerificationDirector.php @@ -0,0 +1,89 @@ +receivedMethodCalls = $receivedMethodCalls; + $this->expectation = $expectation; + } + + public function verify() + { + return $this->receivedMethodCalls->verify($this->expectation); + } + + public function with() + { + return $this->cloneApplyAndVerify("with", func_get_args()); + } + + public function withArgs(array $args) + { + return $this->cloneApplyAndVerify("withArgs", array($args)); + } + + public function withNoArgs() + { + return $this->cloneApplyAndVerify("withNoArgs", array()); + } + + public function withAnyArgs() + { + return $this->cloneApplyAndVerify("withAnyArgs", array()); + } + + public function times($limit = null) + { + return $this->cloneWithoutCountValidatorsApplyAndVerify("times", array($limit)); + } + + public function once() + { + return $this->cloneWithoutCountValidatorsApplyAndVerify("once", array()); + } + + public function twice() + { + return $this->cloneWithoutCountValidatorsApplyAndVerify("twice", array()); + } + + public function atLeast() + { + return $this->cloneWithoutCountValidatorsApplyAndVerify("atLeast", array()); + } + + public function atMost() + { + return $this->cloneWithoutCountValidatorsApplyAndVerify("atMost", array()); + } + + public function between($minimum, $maximum) + { + return $this->cloneWithoutCountValidatorsApplyAndVerify("between", array($minimum, $maximum)); + } + + protected function cloneWithoutCountValidatorsApplyAndVerify($method, $args) + { + $expectation = clone $this->expectation; + $expectation->clearCountValidators(); + call_user_func_array(array($expectation, $method), $args); + $director = new VerificationDirector($this->receivedMethodCalls, $expectation); + $director->verify(); + return $director; + } + + protected function cloneApplyAndVerify($method, $args) + { + $expectation = clone $this->expectation; + call_user_func_array(array($expectation, $method), $args); + $director = new VerificationDirector($this->receivedMethodCalls, $expectation); + $director->verify(); + return $director; + } +} diff --git a/library/Mockery/VerificationExpectation.php b/library/Mockery/VerificationExpectation.php new file mode 100644 index 000000000..660c31fbc --- /dev/null +++ b/library/Mockery/VerificationExpectation.php @@ -0,0 +1,17 @@ +_countValidators = array(); + } + + public function __clone() + { + parent::__clone(); + $this->_actualCount = 0; + } +} diff --git a/tests/Mockery/SpyTest.php b/tests/Mockery/SpyTest.php new file mode 100644 index 000000000..13b1298ab --- /dev/null +++ b/tests/Mockery/SpyTest.php @@ -0,0 +1,79 @@ +container = new \Mockery\Container; + } + + public function teardown() + { + $this->container->mockery_close(); + } + + /** @test */ + public function itVerifiesAMethodWasCalled() + { + $spy = m::spy(); + $spy->myMethod(); + $spy->shouldHaveReceived("myMethod"); + + $this->setExpectedException("Mockery\Exception\InvalidCountException"); + $spy->shouldHaveReceived("someMethodThatWasNotCalled"); + } + + /** @test */ + public function itVerifiesAMethodWasNotCalled() + { + $spy = m::spy(); + $spy->shouldNotHaveReceived("myMethod"); + + $this->setExpectedException("Mockery\Exception\InvalidCountException"); + $spy->myMethod(); + $spy->shouldNotHaveReceived("myMethod"); + } + + /** @test */ + public function itVerifiesAMethodWasNotCalledWithParticularArguments() + { + $spy = m::spy(); + $spy->myMethod(123, 456); + + $spy->shouldNotHaveReceived("myMethod", array(789, 10)); + + $this->setExpectedException("Mockery\Exception\InvalidCountException"); + $spy->shouldNotHaveReceived("myMethod", array(123, 456)); + } + + /** @test */ + public function itVerifiesAMethodWasCalledASpecificNumberOfTimes() + { + $spy = m::spy(); + $spy->myMethod(); + $spy->myMethod(); + $spy->shouldHaveReceived("myMethod")->twice(); + + $this->setExpectedException("Mockery\Exception\InvalidCountException"); + $spy->myMethod(); + $spy->shouldHaveReceived("myMethod")->twice(); + } + + /** @test */ + public function itVerifiesAMethodWasCalledWithSpecificArguments() + { + $spy = m::spy(); + $spy->myMethod(123, "a string"); + $spy->shouldHaveReceived("myMethod")->with(123, "a string"); + $spy->shouldHaveReceived("myMethod", array(123, "a string")); + + $this->setExpectedException("Mockery\Exception\InvalidCountException"); + $spy->shouldHaveReceived("myMethod")->with(123); + } + +}