Skip to content
Browse files

Merge pull request #788 from BlaineSch/feature/mockFeatures

Feature/mock features
  • Loading branch information...
2 parents 397f832 + 8c138f6 commit ddf711f4c19e8ea2bd86efe8ea4bb7b6296de4e3 @nateabele nateabele committed
View
2 analysis/Debugger.php
@@ -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 {
/**
* Used for temporary closure caching.
View
107 test/Mocker.php
@@ -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,6 +121,9 @@ 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(
@@ -130,16 +131,24 @@ class Mocker {
'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');
@@ -287,6 +329,19 @@ protected static function _methodParams(ReflectionMethod $method) {
}
/**
+ * 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.
*
* This pulls from $_mockDelegateIngredients and $_mockIngredients.
@@ -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);
+ }
+
}
?>
View
196 test/MockerChain.php
@@ -0,0 +1,196 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2013, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\test;
+
+/**
+ * Mocker chain is used to aid in assertion of method calls.
+ *
+ * Asserting if `method1` was not called
+ * {{{
+ * $mock = new \lithium\tests\mocks\test\mockStdClass\Mock();
+ * $this->assertFalse(Mocker::chain($mock)->called('method1')->success());
+ * }}}
+ *
+ * Asserting if `method1` was called 2 times
+ * {{{
+ * $mock = new \lithium\tests\mocks\test\mockStdClass\Mock();
+ * $this->assertTrue(Mocker::chain($mock)->called('method1')->eq(2)->success());
+ * }}}
+ *
+ * Asserting if `method2` was called after `method1`
+ * {{{
+ * $mock = new \lithium\tests\mocks\test\mockStdClass\Mock();
+ * $this->assertTrue(Mocker::chain($mock)->called('method1')->called('method2')->success());
+ * }}}
+ *
+ * Asserting if `method2` was called after `method1`, and `method2` had specific arguments.
+ * {{{
+ * $mock = new \lithium\tests\mocks\test\mockStdClass\Mock();
+ * $this->assertTrue(Mocker::chain($mock)
+ * ->called('method1')
+ * ->called('method2')->with('foo', 'bar')
+ * ->success());
+ * }}}
+ */
+class MockerChain extends \lithium\core\Object {
+
+ /**
+ * Data to be used in the class.
+ *
+ * `results` Cached mock results
+ * `method` Method we are asserting
+ * `args` Args we are asserting
+ * `success` Success flag
+ * `callTime` Last method call
+ *
+ * @var array
+ */
+ protected $_data = array(
+ 'results' => null,
+ 'method' => false,
+ 'args' => false,
+ 'success' => true,
+ 'callTime' => 0,
+ );
+
+ /**
+ * Saves the results from the mock.
+ *
+ * @param array $results Results from the mock
+ */
+ public function __construct($results) {
+ $this->_data['results'] = $results;
+ }
+
+ /**
+ * Validates that a given methodis called a set number of times.
+ *
+ * @param string $comparison Comparison type 'gt', 'gte', 'lt', 'lte', or 'eq'.
+ * @param array $args The first argument is the expected result.
+ * @return object
+ */
+ public function __call($comparison, $args) {
+ $methodExists = in_array($comparison, array('gt', 'gte', 'lt', 'lte', 'eq'), true);
+ if (!$this->_data['success'] || !$methodExists) {
+ return $this;
+ }
+ if (count($args) === 0 || !is_int($args[0])) {
+ $this->_data['success'] = false;
+ return $this;
+ }
+ $result = 0;
+ $expected = $args[0];
+ $method = $this->_data['method'];
+ $args = $this->_data['args'];
+ foreach ($this->_data['results'][$method] as $call) {
+ $correctTime = $this->_data['callTime'] <= $call['time'];
+ $correctArgs = !is_array($args) || $args === $call['args'];
+ if ($correctTime && $correctArgs) {
+ $this->_data['callTime'] = $call['time'];
+ $result++;
+ }
+ }
+ switch ($comparison) {
+ case 'gt':
+ $this->_data['success'] = $result > $expected;
+ break;
+ case 'gte':
+ $this->_data['success'] = $result >= $expected;
+ break;
+ case 'lt':
+ $this->_data['success'] = $result < $expected;
+ break;
+ case 'lte':
+ $this->_data['success'] = $result <= $expected;
+ break;
+ case 'eq':
+ $this->_data['success'] = $result === $expected;
+ break;
+ }
+ return $this;
+ }
+
+ /**
+ * Valides the method was called after the last call.
+ *
+ * @param string $method Method to assert
+ * @return object
+ */
+ function called($method) {
+ if (!$this->_data['success']) {
+ return $this;
+ }
+
+ $this->_data['method'] = $method;
+ $this->_data['args'] = false;
+ if (!isset($this->_data['results'][$method])) {
+ $this->_data['success'] = false;
+ return $this;
+ }
+
+ foreach ($this->_data['results'][$method] as $call) {
+ if ($this->_data['callTime'] < $call['time']) {
+ $this->_data['callTime'] = $call['time'];
+ return $this;
+ }
+ }
+
+ $this->_data['success'] = false;
+ return $this;
+ }
+
+ /**
+ * Will further narrow down the original 'called' method.
+ *
+ * Valides the cached method name was called with these args
+ *
+ * @param mixed $arg,... Optional arguments to test against
+ * @return object
+ */
+ public function with() {
+ if (!$this->_data['success']) {
+ return $this;
+ }
+
+ $method = $this->_data['method'];
+ $this->_data['args'] = $args = func_get_args();
+
+ foreach ($this->_data['results'][$method] as $call) {
+ $correctTime = $this->_data['callTime'] <= $call['time'];
+ $correctArgs = $args === $call['args'];
+ if ($correctTime && $correctArgs) {
+ $this->_data['callTime'] = $call['time'];
+ return $this;
+ }
+ }
+
+ $this->_data['success'] = false;
+ return $this;
+ }
+
+ /**
+ * Gives back the success flag
+ *
+ * @return bool
+ */
+ public function success() {
+ $success = $this->_data['success'];
+ $this->_data = array(
+ 'results' => $this->_data['results'],
+ 'method' => false,
+ 'args' => false,
+ 'success' => true,
+ 'callTime' => 0,
+ );
+ return $success;
+ }
+
+}
+
+?>
View
127 tests/cases/test/MockerChainTest.php
@@ -0,0 +1,127 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2013, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\test;
+
+use lithium\test\Mocker;
+
+class MockerChainTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ Mocker::register();
+ }
+
+ public function testStartSuccessful() {
+ $mock = new \lithium\tests\mocks\test\mockStdClass\Mock();
+ $chain = Mocker::chain($mock);
+
+ $this->assertTrue($chain->success());
+ }
+
+ public function testBasicNotCalled() {
+ $mock = new \lithium\tests\mocks\test\mockStdClass\Mock();
+ $chain = Mocker::chain($mock);
+
+ $this->assertFalse($chain->called('method1')->success());
+ }
+
+ public function testBasicCalled() {
+ $mock = new \lithium\tests\mocks\test\mockStdClass\Mock();
+ $mock->method1();
+ $chain = Mocker::chain($mock);
+
+ $this->assertTrue($chain->called('method1')->success());
+ }
+
+ public function testCalledWith() {
+ $mock = new \lithium\tests\mocks\test\mockStdClass\Mock();
+ $mock->method1('foo');
+ $chain = Mocker::chain($mock);
+
+ $this->assertTrue($chain->called('method1')->success());
+ $this->assertFalse($chain->called('method1')->with('bar')->success());
+ $this->assertTrue($chain->called('method1')->with('foo')->success());
+ $this->assertFalse($chain->called('method1')->with('foo', 'bar')->success());
+ }
+
+ public function testMethodCalledBefore() {
+ $mock = new \lithium\tests\mocks\test\mockStdClass\Mock();
+ $mock->method1();
+ $mock->method2();
+ $mock->method1();
+ $chain = Mocker::chain($mock);
+
+ $this->assertTrue($chain->called('method1')
+ ->called('method2')
+ ->called('method1')
+ ->success());
+ $this->assertFalse($chain->called('method2')
+ ->called('method1')
+ ->called('method1')
+ ->success());
+ }
+
+ public function testMethodWithParamsCalledBefore() {
+ $mock = new \lithium\tests\mocks\test\mockStdClass\Mock();
+ $mock->method1('foo');
+ $mock->method2('bar');
+ $mock->method1('baz');
+ $chain = Mocker::chain($mock);
+
+ $this->assertTrue($chain->called('method1')
+ ->called('method2')->with('bar')
+ ->called('method1')
+ ->success());
+ $this->assertFalse($chain->called('method1')->with('bar')
+ ->called('method2')->with('bar')
+ ->called('method1')
+ ->success());
+ $this->assertFalse($chain->called('method1')
+ ->called('method2')->with('bar')
+ ->called('method1')->with('bar')
+ ->success());
+ }
+
+ public function testMethodCalledSpecificTimes() {
+ $mock = new \lithium\tests\mocks\test\mockStdClass\Mock();
+ $mock->method1();
+ $mock->method2();
+ $mock->method1();
+ $chain = Mocker::chain($mock);
+
+ $this->assertFalse($chain->called('method2')->eq(2)->success());
+ $this->assertTrue($chain->called('method1')->eq(2)->success());
+ $this->assertTrue($chain->called('method1')->gt(0)->success());
+ $this->assertTrue($chain->called('method1')->gte(1)->success());
+ $this->assertTrue($chain->called('method1')->lt(3)->success());
+ $this->assertTrue($chain->called('method1')->lte(2)->success());
+ $this->assertFalse($chain->called('method1')->lte(1)->success());
+ }
+
+ public function testMultipleCallsWithArgsAndSpecificCalled() {
+ $mock = new \lithium\tests\mocks\test\mockStdClass\Mock();
+ $mock->method1('foo', 'bar');
+ $mock->method1('foo', 'bar');
+ $mock->method1('foo', 'bar');
+ $mock->method2('baz');
+ $mock->method2('baz');
+ $mock->method1();
+ $chain = Mocker::chain($mock);
+
+ $this->assertTrue($chain->called('method1')->with('foo', 'bar')->eq(3)->success());
+ $this->assertTrue($chain->called('method2')->with('baz')->eq(2)->success());
+ $this->assertTrue($chain->called('method1')->with()->eq(1)->success());
+
+ $this->assertTrue($chain->called('method1')->with('foo', 'bar')->eq(3)
+ ->called('method2')->with('baz')->eq(2)
+ ->called('method1')->with()->eq(1)->success());
+ }
+
+}
+
+?>
View
71 tests/cases/test/MockerTest.php
@@ -12,7 +12,7 @@
/**
* WARNING:
- * No unit test should mock the same test as another
+ * No unit test should mock the same test as another to avoid conflicting filters.
*/
class MockerTest extends \lithium\test\Unit {
@@ -56,13 +56,13 @@ public function testCannotCreateNonStandardMockClass() {
public function testFilteringNonStaticClass() {
$dispatcher = new \lithium\console\dispatcher\Mock();
- $originalResult = $dispatcher->config();
+ $originalResult = $dispatcher->config(array());
$dispatcher->applyFilter('config', function($self, $params, $chain) {
return array();
});
- $filteredResult = $dispatcher->config();
+ $filteredResult = $dispatcher->config(array());
$this->assertEqual(0, count($filteredResult));
$this->assertNotEqual($filteredResult, $originalResult);
@@ -136,7 +136,70 @@ public function testFilteringAFilteredMethod() {
$adapt::applyFilter('_initAdapter', function($self, $params, $chain) {
return false;
});
- $this->assertFalse($adapt::_initAdapter('foo', array()));
+ $this->assertIdentical(false, $adapt::_initAdapter('foo', array()));
+ }
+
+ public function testStaticResults() {
+ $docblock = 'lithium\analysis\docblock\Mock';
+ $docblock::applyFilter(array('comment', 'tags'), function($self, $params, $chain) {
+ return false;
+ });
+ $docblock::comment('foo', 'foobar');
+ $docblock::comment('bar');
+ $docblock::tags('baz');
+
+ $this->assertIdentical(2, count($docblock::$results['comment']));
+ $this->assertIdentical(array('foo', 'foobar'), $docblock::$results['comment'][0]['args']);
+ $this->assertIdentical(false, $docblock::$results['comment'][0]['result']);
+ $this->assertIdentical(array('bar'), $docblock::$results['comment'][1]['args']);
+ $this->assertIdentical(false, $docblock::$results['comment'][1]['result']);
+
+ $this->assertIdentical(1, count($docblock::$results['tags']));
+ $this->assertIdentical(array('baz'), $docblock::$results['tags'][0]['args']);
+ $this->assertIdentical(false, $docblock::$results['tags'][0]['result']);
+ }
+
+ public function testInstanceResults() {
+ $debugger = new \lithium\data\schema\Mock;
+ $debugger->applyFilter(array('names', 'meta'), function($self, $params, $chain) {
+ return false;
+ });
+ $debugger->names('foo', 'foobar');
+ $debugger->names('bar');
+ $debugger->meta('baz');
+
+ $this->assertIdentical(2, count($debugger->results['names']));
+ $this->assertIdentical(array('foo', 'foobar'), $debugger->results['names'][0]['args']);
+ $this->assertIdentical(false, $debugger->results['names'][0]['result']);
+ $this->assertIdentical(array('bar'), $debugger->results['names'][1]['args']);
+ $this->assertIdentical(false, $debugger->results['names'][1]['result']);
+
+ $this->assertIdentical(1, count($debugger->results['meta']));
+ $this->assertIdentical(array('baz'), $debugger->results['meta'][0]['args']);
+ $this->assertIdentical(false, $debugger->results['meta'][0]['result']);
+ }
+
+ public function testSkipByReference() {
+ $stdObj = new \lithium\tests\mocks\test\mockStdClass\Mock();
+ $stdObj->foo = 'foo';
+ $originalData = $stdObj->data();
+ $stdObj->applyFilter('data', function($self, $params, $chain) {
+ return array();
+ });
+ $nonfilteredData = $stdObj->data();
+ $this->assertIdentical($originalData, $nonfilteredData);
+ }
+
+ public function testGetByReference() {
+ $stdObj = new \lithium\tests\mocks\test\mockStdClass\Mock();
+ $stdObj->foo = 'foo';
+ $foo =& $stdObj->foo;
+ $foo = 'bar';
+ $this->assertIdentical('bar', $stdObj->foo);
+ }
+
+ public function testChainReturnsMockerChain() {
+ $this->assertTrue(Mocker::chain(new \stdClass) instanceof \lithium\test\MockerChain);
}
}
View
41 tests/mocks/test/MockStdClass.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace lithium\tests\mocks\test;
+
+class MockStdClass extends \lithium\core\Object {
+
+ protected $_data = array();
+
+ public function __set($key, $value) {
+ return $this->_data[$key] = $value;
+ }
+
+ public function &__get($key) {
+ if (isset($this->_data[$key])) {
+ $data =& $this->_data[$key];
+ return $data;
+ }
+ $data = null;
+ return $data;
+ }
+
+ public function &data() {
+ $data =& $this->_data;
+ return $data;
+ }
+
+ public function filterableData() {
+ return $this->_data;
+ }
+
+ public function method1() {
+ return true;
+ }
+
+ public function method2() {
+ return false;
+ }
+
+}
+
+?>

0 comments on commit ddf711f

Please sign in to comment.
Something went wrong with that request. Please try again.