Skip to content

Commit

Permalink
Merge pull request #283 from davedevelopment/spies
Browse files Browse the repository at this point in the history
Basic/naive spy implementation
  • Loading branch information
davedevelopment committed Oct 7, 2014
2 parents 627a6c8 + 31866ac commit aab7d1c
Show file tree
Hide file tree
Showing 10 changed files with 360 additions and 61 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,9 @@
# Change Log # Change Log


## 0.9.3 (XXXX-XX-XX)

* Added a basic spy implementation

## 0.9.2 (2014-09-03) ## 0.9.2 (2014-09-03)


* Some workarounds for the serilisation problems created by changes to PHP in 5.5.13, 5.4.29, * Some workarounds for the serilisation problems created by changes to PHP in 5.5.13, 5.4.29,
Expand Down
11 changes: 9 additions & 2 deletions library/Mockery.php
Expand Up @@ -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 * @return \Mockery\MockInterface
*/ */
public static function instanceMock() public static function instanceMock()
Expand Down
5 changes: 5 additions & 0 deletions library/Mockery/Expectation.php
Expand Up @@ -693,4 +693,9 @@ public function __clone()
$this->_countValidators = $newValidators; $this->_countValidators = $newValidators;
} }


public function getName()
{
return $this->_name;
}

} }
Expand Up @@ -73,7 +73,7 @@ protected function appendToClass($class, $code)


private function renderMethodBody($method, $config) private function renderMethodBody($method, $config)
{ {
$invoke = $method->isStatic() ? 'static::__callStatic' : '$this->__call'; $invoke = $method->isStatic() ? 'static::_mockery_handleStaticMethodCall' : '$this->_mockery_handleMethodCall';
$body = <<<BODY $body = <<<BODY
{ {
\$argc = func_num_args(); \$argc = func_num_args();
Expand Down
25 changes: 25 additions & 0 deletions library/Mockery/MethodCall.php
@@ -0,0 +1,25 @@
<?php

namespace Mockery;

class MethodCall
{
private $method;
private $args;

public function __construct($method, $args)
{
$this->method = $method;
$this->args = $args;
}

public function getMethod()
{
return $this->method;
}

public function getArgs()
{
return $this->args;
}
}
159 changes: 101 additions & 58 deletions library/Mockery/Mock.php
Expand Up @@ -137,6 +137,8 @@ class Mock implements MockInterface


protected $_mockery_allowMockingProtectedMethods = false; protected $_mockery_allowMockingProtectedMethods = false;


protected $_mockery_receivedMethodCalls;

/** /**
* If shouldIgnoreMissing is called, this value will be returned on all calls to missing methods * If shouldIgnoreMissing is called, this value will be returned on all calls to missing methods
* @var mixed * @var mixed
Expand Down Expand Up @@ -312,68 +314,12 @@ public function byDefault()
*/ */
public function __call($method, array $args) public function __call($method, array $args)
{ {
$rm = $this->mockery_getMethod($method); return $this->_mockery_handleMethodCall($method, $args);
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'
);
} }


public static function __callStatic($method, array $args) public static function __callStatic($method, array $args)
{ {
try { return self::_mockery_handleStaticMethodCall($method, $args);
$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'
);
}
} }


/** /**
Expand Down Expand Up @@ -656,6 +602,103 @@ public function mockery_getMethod($name)
return null; 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() protected function mockery_getMethods()
{ {
if (static::$_mockery_methods) { if (static::$_mockery_methods) {
Expand Down
30 changes: 30 additions & 0 deletions library/Mockery/ReceivedMethodCalls.php
@@ -0,0 +1,30 @@
<?php

namespace Mockery;

class ReceivedMethodCalls
{
private $methodCalls = array();

public function push(MethodCall $methodCall)
{
$this->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();
}
}
89 changes: 89 additions & 0 deletions library/Mockery/VerificationDirector.php
@@ -0,0 +1,89 @@
<?php

namespace Mockery;

class VerificationDirector
{
private $receivedMethodCalls;
private $expectation;

public function __construct(ReceivedMethodCalls $receivedMethodCalls, VerificationExpectation $expectation)
{
$this->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;
}
}
17 changes: 17 additions & 0 deletions library/Mockery/VerificationExpectation.php
@@ -0,0 +1,17 @@
<?php

namespace Mockery;

class VerificationExpectation extends Expectation
{
public function clearCountValidators()
{
$this->_countValidators = array();
}

public function __clone()
{
parent::__clone();
$this->_actualCount = 0;
}
}

0 comments on commit aab7d1c

Please sign in to comment.