Permalink
Browse files

Fix bug with how results are rendered.

The issue was we were rendering results BEFORE the filter, passing it in and returning it. This is bad since the results may modify the existing class and being a filter you may choose never to get that far in  your chain.

The fix is a bit complex but covers my bases.

You create a class 'Mock' which extends the class you wish to mock, this will be directly accessed by the consumer, but it doesnt interact with itself, it sends all its responsibilities to another class we create 'MockDelegate'.

MockDelegate which, if called from the Mock class will pass it along to the real mocked class, or if called from the extended class, will submit it back out through the client Mock class. Now all methods are filtered.

A known issue is not something I can really fix.
~~~ php
<?php
class Foo {
	public static function doSomething() {
		return self::doSomethingElse();
	}
	public static function doSomethingElse() {
		return false;
	}
}
?>
~~~
If you are Mocking this class the `doSomething` methods NEVER goes to the extending 'MockDelegate' to be re-filtered so this method would NOT be filtered even if you have set a filter for this method.
  • Loading branch information...
1 parent d69563f commit 0803c888fe7337d342e0f281baf53cfbdcb55cc0 @blainesch blainesch committed with Blaine Schmeisser Dec 20, 2012
Showing with 159 additions and 35 deletions.
  1. +133 −35 test/Mocker.php
  2. +26 −0 tests/cases/test/MockerTest.php
View
168 test/Mocker.php
@@ -62,34 +62,119 @@
class Mocker {
/**
- * A list of code to be generated based on the type.
+ * A list of code to be generated for the delegator.
+ *
+ * The MockDelgate directly extends the mocker and makes all methods
+ * publically available to other classes but should not be accessed directly
+ * by any other application. This should be called only by the mocker and
+ * the mockee and never by the consumer.
*
* @var array
*/
- protected static $_dynamicCode = array(
+ protected static $_mockDelegateIngredients = array(
'startClass' => array(
'namespace {:namespace};',
- 'class {:mockee} extends \{:mocker} {'
+ 'class MockDelegate extends \{:mocker} {'
+ ),
+ 'constructor' => array(
+ '{:modifiers} function __construct({:args}) {',
+ ' $args = func_get_args();',
+ ' $this->parent = array_pop($args);',
+ ' $this->parent->mocker = $this;',
+ ' call_user_func_array("parent::__construct", $args);',
+ '}',
+ ),
+ 'method' => array(
+ '{:modifiers} function {:method}({:args}) {',
+ ' $args = func_get_args();',
+ ' $token = spl_object_hash($this);',
+ ' $id = count($args) - 1;',
+ ' if (!isset($args[$id]) || $args[$id] !== $token) {',
+ ' $method = array($this->parent, "{:method}");',
+ ' return call_user_func_array($method, $args);',
+ ' }',
+ ' array_pop($args);',
+ ' return call_user_func_array("parent::{:method}", $args);',
+ '}',
+ ),
+ 'staticMethod' => array(
+ '{:modifiers} function {:method}({:args}) {',
+ ' $args = func_get_args();',
+ ' $token = "1f3870be274f6c49b3e31a0c6728957f";',
+ ' $id = count($args) - 1;',
+ ' if (!isset($args[$id]) || $args[$id] !== $token) {',
+ ' $method = \'{:namespace}\Mock::{:method}\';',
+ ' return call_user_func_array($method, $args);',
+ ' }',
+ ' array_pop($args);',
+ ' return call_user_func_array("parent::{:method}", $args);',
+ '}',
+ ),
+ 'endClass' => array(
+ '}',
+ ),
+ );
+
+ /**
+ * A list of code to be generated for the mocker.
+ *
+ * The Mock class directly extends the mock class but only directly
+ * interacts with the MockDelegate directly. This class is the actual
+ * interface for consumers, instantiation or static method calls, and can
+ * have most of its methods filtered.
+ *
+ * @var array
+ */
+ protected static $_mockIngredients = array(
+ 'startClass' => array(
+ 'namespace {:namespace};',
+ 'class Mock extends \{:mocker} {',
+ ' public $mocker;',
+ ' protected $_safeVars = array(',
+ ' "_classes",',
+ ' "_methodFilters",',
+ ' "mocker",',
+ ' "_safeVars"',
+ ' );',
+ ),
+ 'get' => array(
+ 'public function __get($key) {',
+ ' return $this->mocker->$key;',
+ '}',
+ ),
+ 'constructor' => array(
+ '{:modifiers} function __construct({:args}) {',
+ ' $args = array_values(get_defined_vars());',
+ ' array_push($args, $this);',
+ ' foreach ($this as $key => $value) {',
+ ' if (!in_array($key, $this->_safeVars)) {',
+ ' unset($this->$key);',
+ ' }',
+ ' }',
+ ' $class = new \ReflectionClass(\'{:namespace}\MockDelegate\');',
+ ' $class->newInstanceArgs($args);',
+ '}',
+ ),
+ 'destructor' => array(
+ 'public function __destruct() {}',
),
'staticMethod' => array(
- '{:modifiers} function {:name}({:params}) {',
- ' $params = func_get_args();',
- ' list($class, $method) = explode(\'::\', __METHOD__, 2);',
- ' $parent = \'parent::\' . $method;',
- ' $result = call_user_func_array($parent, $params);',
- ' return self::_filter($method, $params, function($self, $params) use(&$result) {',
- ' return $result;',
+ '{:modifiers} function {:method}({:args}) {',
+ ' $args = func_get_args();',
+ ' array_push($args, "1f3870be274f6c49b3e31a0c6728957f");',
+ ' $method = \'{:namespace}\MockDelegate::{:method}\';',
+ ' return self::_filter("{:method}", $args, function($self, $args) use(&$method) {',
+ ' return call_user_func_array($method, $args);',
' });',
'}',
),
'method' => array(
- '{:modifiers} function {:name}({:params}) {',
- ' $params = func_get_args();',
- ' list($class, $method) = explode(\'::\', __METHOD__, 2);',
- ' $parent = \'parent::\' . $method;',
- ' $result = call_user_func_array($parent, $params);',
- ' return $this->_filter($parent, $params, function($self, $params) use(&$result) {',
- ' return $result;',
+ '{:modifiers} function {:method}({:args}) {',
+ ' $args = func_get_args();',
+ ' array_push($args, spl_object_hash($this->mocker));',
+ ' $method = array($this->mocker, "{:method}");',
+ ' return $this->_filter(__METHOD__, $args, function($self, $args) use(&$method) {',
+ ' return call_user_func_array($method, $args);',
' });',
'}',
),
@@ -104,12 +189,11 @@ class Mocker {
* @var array
*/
protected static $_blackList = array(
- '__construct', '__destruct', '__call', '__callStatic',
+ '__destruct', '__call', '__callStatic', '_parents',
'__get', '__set', '__isset', '__unset', '__sleep',
'__wakeup', '__toString', '__clone', '__invoke',
- '__construct', '_init', 'applyFilter', 'invokeMethod',
- '__set_state', '_instance', '_filter', '_parents',
- '_stop',
+ '_stop', '_init', 'applyFilter', 'invokeMethod',
+ '__set_state', '_instance', '_filter',
);
/**
@@ -134,29 +218,38 @@ public static function create($mockee) {
$mocker = self::_mocker($mockee);
- $code = self::_dynamicCode('startClass', array(
+ $tokens = array(
'namespace' => self::_namespace($mockee),
'mocker' => $mocker,
- 'mockee' => 'Mock',
- ));
+ 'mockee' => 'MockDelegate',
+ );
+ $mockDelegate = self::_dynamicCode('mockDelegate', 'startClass', $tokens);
+ $mock = self::_dynamicCode('mock', 'startClass', $tokens);
$reflectedClass = new ReflectionClass($mocker);
$reflecedMethods = $reflectedClass->getMethods();
foreach ($reflecedMethods as $method) {
if (!in_array($method->name, self::$_blackList)) {
$key = $method->isStatic() ? 'staticMethod' : 'method';
- $code .= self::_dynamicCode($key, array(
- 'name' => $method->name,
+ $key = $method->name === '__construct' ? 'constructor' : $key;
+ $tokens = array(
+ 'namespace' => self::_namespace($mockee),
+ 'method' => $method->name,
'modifiers' => self::_methodModifiers($method),
- 'params' => self::_methodParams($method),
- 'visibility' => 'public',
- ));
+ 'args' => self::_methodParams($method),
+ 'mocker' => $mocker,
+ );
+ $mockDelegate .= self::_dynamicCode('mockDelegate', $key, $tokens);
+ $mock .= self::_dynamicCode('mock', $key, $tokens);
}
}
- $code .= self::_dynamicCode('endClass');
+ $mockDelegate .= self::_dynamicCode('mockDelegate', 'endClass');
+ $mock .= self::_dynamicCode('mock', 'get');
+ $mock .= self::_dynamicCode('mock', 'destructor');
+ $mock .= self::_dynamicCode('mock', 'endClass');
- eval($code);
+ eval($mockDelegate . $mock);
}
/**
@@ -170,7 +263,8 @@ public static function create($mockee) {
protected static function _methodModifiers(ReflectionMethod $method) {
$modifierKey = $method->getModifiers();
$modifierArray = Reflection::getModifierNames($modifierKey);
- return implode(' ', $modifierArray);
+ $modifiers = implode(' ', $modifierArray);
+ return str_replace(array('private', 'protected'), 'public', $modifiers);
}
/**
@@ -195,12 +289,16 @@ protected static function _methodParams(ReflectionMethod $method) {
/**
* Will generate the code you are wanting.
*
- * @param string $key The key from self::$_dynamicCode
+ * This pulls from $_mockDelegateIngredients and $_mockIngredients.
+ *
+ * @param string $type The name of the array of ingredients to use
+ * @param string $key The key from the array of ingredients
* @param array $tokens Tokens, if any, that should be inserted
* @return string
*/
- protected static function _dynamicCode($key, $tokens = array()) {
- $code = implode("\n", self::$_dynamicCode[$key]);
+ protected static function _dynamicCode($type, $key, $tokens = array()) {
+ $name = '_' . $type . 'Ingredients';
+ $code = implode("\n", self::${$name}[$key]);
return String::insert($code, $tokens) . "\n";
}
View
26 tests/cases/test/MockerTest.php
@@ -113,6 +113,32 @@ public function testFilteringStaticClassCanReturnOriginal() {
$this->assertEqual($filteredResult, $originalResult);
}
+ public function testOriginalMethodNotCalled() {
+ $http = new \lithium\tests\mocks\security\auth\adapter\mockHttp\Mock;
+
+ $this->assertEqual(0, count($http->headers));
+
+ $http->_writeHeader('Content-type: text/html');
+
+ $this->assertEqual(1, count($http->headers));
+
+ $http->applyFilter('_writeHeader', function($self, $params, $chain) {
+ return false;
+ });
+
+ $http->_writeHeader('Content-type: application/pdf');
+
+ $this->assertEqual(1, count($http->headers));
+ }
+
+ public function testFilteringAFilteredMethod() {
+ $adapt = 'lithium\core\adaptable\Mock';
+ $adapt::applyFilter('_initAdapter', function($self, $params, $chain) {
+ return false;
+ });
+ $this->assertFalse($adapt::_initAdapter('foo', array()));
+ }
+
}
?>

0 comments on commit 0803c88

Please sign in to comment.