Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Feature/autoload mocker with filters #762

Merged
merged 2 commits into from

2 participants

@blainesch
Collaborator

Fix for original #755

Same stats:
Code Complexity: 1.75
Code Coverage: 100%

@blainesch
Collaborator

If there's a single commit they throw the long description into the pull request description. There were two commits so that didn't happen. You can look at the last commit for a good explanation of what/why changed.

@blainesch blainesch 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.
0803c88
@nateabele nateabele merged commit dda0ddf into UnionOfRAD:dev

1 check passed

Details default The Travis build passed
@blainesch blainesch deleted the unknown repository branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Dec 20, 2012
  1. @blainesch
Commits on Dec 21, 2012
  1. @blainesch

    Fix bug with how results are rendered.

    blainesch authored Blaine Schmeisser committed
    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.
This page is out of date. Refresh to see the latest.
Showing with 203 additions and 36 deletions.
  1. +177 −36 test/Mocker.php
  2. +26 −0 tests/cases/test/MockerTest.php
View
213 test/Mocker.php
@@ -14,39 +14,167 @@
use Reflection;
/**
- * The Mocker class aids in the creation of Mocks on the fly.
+ * The Mocker class aids in the creation of Mocks on the fly, allowing you to
+ * use Lithium filters on most methods in the class.
+ *
+ * To enable the autoloading of mocks you simply need to make a simple method
+ * call.
+ * {{{
+ * use lithium\core\Environment;
+ * use lithium\test\Mocker;
+ * if(!Environment::is('production')) {
+ * Mocker::register();
+ * }
+ * }}}
+ *
+ * You can also enable autoloading inside the setup of a unit test class. This
+ * method can be called redundantly.
+ * {{{
+ * use lithium\test\Mocker;
+ * class MockerTest extends \lithium\test\Unit {
+ * public function setUp() {
+ * Mocker::register();
+ * }
+ * }
+ * }}}
+ *
+ * Using Mocker is the fun magical part, it's autoloaded so simply call the
+ * class you want to mock with the '\Mock' at the end. The autoloader will
+ * detect you want to autoload it, and create it for you. Now you can filter
+ * any method.
+ * {{{
+ * use lithium\console\dispatcher\Mock as DispatcherMock;
+ * $dispatcher = new DispatcherMock();
+ * $dispatcher->applyFilter('config', function($self, $params, $chain) {
+ * return array();
+ * });
+ * $results = $dispatcher->config();
+ * }}}
+ * {{{
+ * use lithium\analysis\parser\Mock as ParserMock;
+ * $code = 'echo "foobar";';
+ * ParserMock::applyFilter('config', function($self, $params, $chain) {
+ * return array();
+ * });
+ * $tokens = ParserMock::tokenize($code, array('wrap' => true));
+ * }}}
*/
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);',
' });',
'}',
),
@@ -61,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',
);
/**
@@ -91,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);
}
/**
@@ -127,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);
}
/**
@@ -152,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()));
+ }
+
}
?>
Something went wrong with that request. Please try again.