Feature/mock features #788

Merged
merged 3 commits into from Jan 18, 2013
Jump to file or symbol
Failed to load files and symbols.
+521 −23
Split
View
@@ -16,7 +16,7 @@
* The `Debugger` class provides basic facilities for generating and rendering meta-data about the
* state of an application in its current context.
*/
-class Debugger extends \lithium\core\Object {
+class Debugger extends \lithium\core\StaticObject {
@nateabele

nateabele Jan 17, 2013

Owner

A quick scan of Debugger seems to show it doesn't really need to extend anything. Thoughts?

@blainesch

blainesch Jan 17, 2013

Member

I assumed it was an unwritten lithium convention to always extend Object or StaticObject? It'll probably make it faster not to extend anything, but without benchmarks I'd probably say very minimal performance improvement. I like that most objects extend core functionality.

The only side effect I can think of is if it doesn't extend one of them it cannot be used by Mocker since it will no longer have access to applyFilter.

@nateabele

nateabele Jan 17, 2013

Owner

I assumed it was an unwritten lithium convention to always extend Object or StaticObject?

Really only when necessary.

The only side effect I can think of is if it doesn't extend one of them it cannot be used by Mocker since it will no longer have access to applyFilter.

I'm not sure I follow, since Debugger doesn't have any filterable methods.

@blainesch

blainesch Jan 17, 2013

Member

Right, no filterable methods exist. However if I try to autoload lithium\analysis\debugger\Mock now all (well most) methods are filterable. However if the class doesn't extend Object or StaticObject the Mocker autoloader won't even try to create it.

@nateabele

nateabele Jan 17, 2013

Owner

Okay, gotcha. Never mind then, carry on. :-)

@jails

jails Jan 17, 2013

Contributor

Pure hypothesis, since we need to override all methods in the Mock to make them filterable, can't we add to Mock it's own Object + StaticObject logic ?
i.e.

  • Mock will simply be a kind of "proxy" to the mocked class/instance.
  • an simple if (isset($this)) allow to differenciate Mock::applyFilter from $mock->applyFilter call.
  • According the ReflectionClass we know if a method is static or not so we can build the correct filter.

This way the Mocker can Mock _any kind of class_ with a small "footprints" (i.e adding applyFilter, _filter and $_methodFilters to the attributes/methods). And this way Debugger doesn't need to extends anything to be mocked.

Does that make sense ?

@blainesch

blainesch Jan 18, 2013

Member

That's an interesting idea. It sounds like it would work, I'll try and tackle that for my next pr. It would be nice to be able to 'mock' any class.

@nateabele If there are no other side effects of not extending Object or StaticObject, to increase performance would a pr be accepted that looked through and manually pulled out unnecessary extensions? Or do you think it's too few to be worth the time?

@jails

jails Jan 18, 2013

Contributor

As an afterthought, looks like mixing Object and StaticObject will break the strict standards (i.e. Non-static method should not be called statically). Moreover Models override the default StaticObject filter system so here the Mock must extends Model to make it work.

However I'm pretty sure we can find an alternative by dynamically extending the Mock from the mocked class if the mocked extends StaticObject or Object. And for other kind of class, the Mock will extends StaticObject or Object depending on "something" (something can be replaced here by a big question mark). Or maybe using two keywords Mock|StaticMock which indicate the type of the class ? Need to think about it.

@nateabele

nateabele Jan 18, 2013

Owner

@blainesch The thing with Debugger is that there's really no reason anyone would ever extend it. The thing with going through every class in the codebase is that there are very few other classes for which that is definitively, absolutely true. So yeah, not worth it, IMO.

/**
* Used for temporary closure caching.
View
@@ -22,7 +22,7 @@
* {{{
* use lithium\core\Environment;
* use lithium\test\Mocker;
- * if(!Environment::is('production')) {
+ * if (!Environment::is('production')) {
* Mocker::register();
* }
* }}}
@@ -93,8 +93,7 @@ class Mocker {
' $method = array($this->parent, "{:method}");',
' return call_user_func_array($method, $args);',
' }',
- ' array_pop($args);',
- ' return call_user_func_array("parent::{:method}", $args);',
+ ' return call_user_func_array("parent::{:method}", compact({:stringArgs}));',
'}',
),
'staticMethod' => array(
@@ -106,8 +105,7 @@ class Mocker {
' $method = \'{:namespace}\Mock::{:method}\';',
' return call_user_func_array($method, $args);',
' }',
- ' array_pop($args);',
- ' return call_user_func_array("parent::{:method}", $args);',
+ ' return call_user_func_array("parent::{:method}", compact({:stringArgs}));',
'}',
),
'endClass' => array(
@@ -123,23 +121,34 @@ class Mocker {
* interface for consumers, instantiation or static method calls, and can
* have most of its methods filtered.
*
+ * The `$results` variable holds all method calls allowing you for you
+ * make your own custom assertions on them.
+ *
* @var array
*/
protected static $_mockIngredients = array(
'startClass' => array(
'namespace {:namespace};',
'class Mock extends \{:mocker} {',
' public $mocker;',
+ ' public {:static} $results = array();',
' protected $_safeVars = array(',
' "_classes",',
' "_methodFilters",',
' "mocker",',
- ' "_safeVars"',
+ ' "_safeVars",',
+ ' "results",',
' );',
),
'get' => array(
- 'public function __get($key) {',
- ' return $this->mocker->$key;',
+ 'public function {:reference}__get($name) {',
+ ' $data ={:reference} $this->mocker->$name;',
+ ' return $data;',
+ '}',
+ ),
+ 'set' => array(
+ 'public function __set($name, $value = null) {',
+ ' return $this->mocker->$name = $value;',
'}',
),
'constructor' => array(
@@ -160,22 +169,40 @@ class Mocker {
),
'staticMethod' => array(
'{:modifiers} function {:method}({:args}) {',
- ' $args = func_get_args();',
- ' array_push($args, "1f3870be274f6c49b3e31a0c6728957f");',
+ ' $args = compact({:stringArgs});',
+ ' $args["hash"] = "1f3870be274f6c49b3e31a0c6728957f";',
' $method = \'{:namespace}\MockDelegate::{:method}\';',
- ' return self::_filter("{:method}", $args, function($self, $args) use(&$method) {',
+ ' $result = self::_filter("{:method}", $args, function($self, $args) use(&$method) {',
' return call_user_func_array($method, $args);',
' });',
+ ' if (!isset(self::$results["{:method}"])) {',
+ ' self::$results["{:method}"] = array();',
+ ' }',
+ ' self::$results["{:method}"][] = array(',
+ ' "args" => func_get_args(),',
+ ' "result" => $result,',
+ ' "time" => microtime(true),',
+ ' );',
+ ' return $result;',
'}',
),
'method' => array(
'{:modifiers} function {:method}({:args}) {',
- ' $args = func_get_args();',
- ' array_push($args, spl_object_hash($this->mocker));',
+ ' $args = compact({:stringArgs});',
+ ' $args["hash"] = spl_object_hash($this->mocker);',
' $method = array($this->mocker, "{:method}");',
- ' return $this->_filter(__METHOD__, $args, function($self, $args) use(&$method) {',
+ ' $result = $this->_filter(__METHOD__, $args, function($self, $args) use(&$method) {',
' return call_user_func_array($method, $args);',
' });',
+ ' if (!isset($this->results["{:method}"])) {',
+ ' $this->results["{:method}"] = array();',
+ ' }',
+ ' $this->results["{:method}"][] = array(',
+ ' "args" => func_get_args(),',
+ ' "result" => $result,',
+ ' "time" => microtime(true),',
+ ' );',
+ ' return $result;',
'}',
),
'endClass' => array(
@@ -192,8 +219,9 @@ class Mocker {
'__destruct', '__call', '__callStatic', '_parents',
'__get', '__set', '__isset', '__unset', '__sleep',
'__wakeup', '__toString', '__clone', '__invoke',
- '_stop', '_init', 'applyFilter', 'invokeMethod',
- '__set_state', '_instance', '_filter',
+ '_stop', '_init', 'invokeMethod', '__set_state',
+ '_instance', '_filter', '_object', '_initialize',
+ 'applyFilter',
);
/**
@@ -217,35 +245,49 @@ public static function create($mockee) {
}
$mocker = self::_mocker($mockee);
+ $isStatic = is_subclass_of($mocker, 'lithium\core\StaticObject');
$tokens = array(
'namespace' => self::_namespace($mockee),
'mocker' => $mocker,
'mockee' => 'MockDelegate',
+ 'static' => $isStatic ? 'static' : '',
);
$mockDelegate = self::_dynamicCode('mockDelegate', 'startClass', $tokens);
$mock = self::_dynamicCode('mock', 'startClass', $tokens);
$reflectedClass = new ReflectionClass($mocker);
$reflecedMethods = $reflectedClass->getMethods();
- foreach ($reflecedMethods as $method) {
+ $getByReference = false;
+ foreach ($reflecedMethods as $methodId => $method) {
if (!in_array($method->name, self::$_blackList)) {
$key = $method->isStatic() ? 'staticMethod' : 'method';
$key = $method->name === '__construct' ? 'constructor' : $key;
+ $docs = ReflectionMethod::export($mocker, $method->name, true);
+ if (preg_match('/&' . $method->name . '/', $docs) === 1) {
+ continue;
+ }
$tokens = array(
'namespace' => self::_namespace($mockee),
'method' => $method->name,
'modifiers' => self::_methodModifiers($method),
'args' => self::_methodParams($method),
+ 'stringArgs' => self::_stringMethodParams($method),
'mocker' => $mocker,
);
$mockDelegate .= self::_dynamicCode('mockDelegate', $key, $tokens);
$mock .= self::_dynamicCode('mock', $key, $tokens);
+ } elseif ($method->name === '__get') {
+ $docs = ReflectionMethod::export($mocker, '__get', true);
+ $getByReference = preg_match('/&__get/', $docs) === 1;
}
}
$mockDelegate .= self::_dynamicCode('mockDelegate', 'endClass');
- $mock .= self::_dynamicCode('mock', 'get');
+ $mock .= self::_dynamicCode('mock', 'get', array(
+ 'reference' => $getByReference ? '&' : '',
+ ));
+ $mock .= self::_dynamicCode('mock', 'set');
$mock .= self::_dynamicCode('mock', 'destructor');
$mock .= self::_dynamicCode('mock', 'endClass');
@@ -286,6 +328,19 @@ protected static function _methodParams(ReflectionMethod $method) {
return str_replace($replace['from'], $replace['to'], $params);
}
+ /**
+ * Will return the params in a way that can be placed into `compact()`
+ *
+ * @param ReflectionMethod $method
+ * @return string
+ */
+ protected static function _stringMethodParams(ReflectionMethod $method) {
+ $pattern = '/Parameter [^$]+\$([^ ]+)/';
+ preg_match_all($pattern, $method, $matches);
+ $params = implode("', '", $matches[1]);
+ return strlen($params) > 0 ? "'{$params}'" : 'array()';
+ }
+
/**
* Will generate the code you are wanting.
*
@@ -348,6 +403,22 @@ protected static function _validateMockee($mockee) {
return true;
}
+ /**
+ * Generate a chain class with the current rules of the mock.
+ *
+ * @param object $mock Mock to chain
+ * @return object MockerChain instance
+ */
+ public static function chain($mock) {
+ $results = array();
+ if (is_object($mock) && isset($mock->results)) {
+ $results = $mock->results;
+ } elseif (is_string($mock) && class_exists($mock) && isset($mock::$results)) {
+ $results = $mock::$results;
+ }
+ return new MockerChain($results);
+ }
+
}
?>
Oops, something went wrong.