Permalink
Browse files

Add a lithium mocking class with an autoloader.

I do not like how the code is currently being stored in a variable
on the class.

I do not like how I am using eval to get this job done.

Code Complexity: 1.75
Code Coverage: 100%
  • Loading branch information...
1 parent 16969a8 commit 195c6b8ec32d9c3491d33568118091ef95de9611 Blaine Schmeisser committed Dec 19, 2012
Showing with 330 additions and 0 deletions.
  1. +212 −0 test/Mocker.php
  2. +118 −0 tests/cases/test/MockerTest.php
View
@@ -0,0 +1,212 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2012, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\test;
+
+use lithium\util\String;
+use ReflectionClass;
+use ReflectionMethod;
+use Reflection;
+
+/**
+ * The Mocker class aids in the creation of Mocks on the fly.
+ */
+class Mocker {
+
+ /**
+ * A list of code to be generated based on the type.
+ *
+ * @var array
+ */
+ protected static $_dynamicCode = array(
+ 'startClass' => array(
+ 'namespace {:namespace};',
+ 'class {:mockee} extends \{:mocker} {'
+ ),
+ '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;',
+ ' });',
+ '}',
+ ),
+ '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;',
+ ' });',
+ '}',
+ ),
+ 'endClass' => array(
+ '}',
+ ),
+ );
+
+ /**
+ * A list of methods we should not overwrite in our mock class.
+ *
+ * @var array
+ */
+ protected static $_blackList = array(
+ '__construct', '__destruct', '__call', '__callStatic',
+ '__get', '__set', '__isset', '__unset', '__sleep',
+ '__wakeup', '__toString', '__clone', '__invoke',
+ '__construct', '_init', 'applyFilter', 'invokeMethod',
+ '__set_state', '_instance', '_filter', '_parents',
+ '_stop',
+ );
+
+ /**
+ * Will register this class into the autoloader.
+ *
+ * @return void
+ */
+ public static function register() {
+ spl_autoload_register(array(__CLASS__, 'create'));
+ }
+
+ /**
+ * The main entrance to create a new Mock class.
+ *
+ * @param string $mockee The fully namespaced `\Mock` class
+ * @return void
+ */
+ public static function create($mockee) {
+ if (!self::_validateMockee($mockee)) {
+ return;
+ }
+
+ $mocker = self::_mocker($mockee);
+
+ $code = self::_dynamicCode('startClass', array(
+ 'namespace' => self::_namespace($mockee),
+ 'mocker' => $mocker,
+ 'mockee' => 'Mock',
+ ));
+
+ $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,
+ 'modifiers' => self::_methodModifiers($method),
+ 'params' => self::_methodParams($method),
+ 'visibility' => 'public',
+ ));
+ }
+ }
+
+ $code .= self::_dynamicCode('endClass');
+
+ eval($code);
+ }
+
+ /**
+ * Will determine what method mofifiers of a method.
+ *
+ * For instance: 'public static' or 'private abstract'
+ *
+ * @param ReflectionMethod $method
+ * @return string
+ */
+ protected static function _methodModifiers(ReflectionMethod $method) {
+ $modifierKey = $method->getModifiers();
+ $modifierArray = Reflection::getModifierNames($modifierKey);
+ return implode(' ', $modifierArray);
+ }
+
+ /**
+ * Will determine what parameter prototype of a method.
+ *
+ * For instance: 'ReflectionMethod $method' or '$name, array $foo = null'
+ *
+ * @param ReflectionMethod $method
+ * @return string
+ */
+ protected static function _methodParams(ReflectionMethod $method) {
+ $pattern = '/Parameter #[0-9]+ \[ [^\>]+>([^\]]+) \]/';
+ $replace = array(
+ 'from' => array('Array', 'or NULL'),
+ 'to' => array('array()', ''),
+ );
+ preg_match_all($pattern, $method, $matches);
+ $params = implode(', ', $matches[1]);
+ return str_replace($replace['from'], $replace['to'], $params);
+ }
+
+ /**
+ * Will generate the code you are wanting.
+ *
+ * @param string $key The key from self::$_dynamicCode
+ * @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]);
+ return String::insert($code, $tokens) . "\n";
+ }
+
+ /**
+ * Will generate the mocker from the current mockee.
+ *
+ * @param string $mockee The fully namespaced `\Mock` class
+ * @return array
+ */
+ protected static function _mocker($mockee) {
+ $matches = array();
+ preg_match_all('/^(.*)\\\\([^\\\\]+)\\\\Mock$/', $mockee, $matches);
+ if (!isset($matches[1][0])) {
+ return;
+ }
+ return $matches[1][0] . '\\' . ucfirst($matches[2][0]);
+ }
+
+ /**
+ * Will generate the namespace from the current mockee.
+ *
+ * @param string $mockee The fully namespaced `\Mock` class
+ * @return string
+ */
+ protected static function _namespace($mockee) {
+ $matches = array();
+ preg_match_all('/^(.*)\\\\Mock$/', $mockee, $matches);
+ return isset($matches[1][0]) ? $matches[1][0] : null;
+ }
+
+ /**
+ * Will validate if mockee is a valid class we should mock.
+ *
+ * @param string $mockee The fully namespaced `\Mock` class
+ * @return bool
+ */
+ protected static function _validateMockee($mockee) {
+ if (class_exists($mockee) || preg_match('/\\\\Mock$/', $mockee) !== 1) {
+ return false;
+ }
+ $mocker = self::_mocker($mockee);
+ $isObject = is_subclass_of($mocker, 'lithium\core\Object');
+ $isStatic = is_subclass_of($mocker, 'lithium\core\StaticObject');
+ if (!$isObject && !$isStatic) {
+ return false;
+ }
+ return true;
+ }
+
+}
+
+?>
@@ -0,0 +1,118 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2012, 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;
+
+/**
+ * WARNING:
+ * No unit test should mock the same test as another
+ */
+class MockerTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ Mocker::register();
+ }
+
+ public function testAutoloadRegister() {
+ Mocker::register();
+ $registered = spl_autoload_functions();
+ $this->assertTrue(in_array(array(
+ 'lithium\test\Mocker',
+ 'create'
+ ), $registered));
+ }
+
+ public function testBasicCreation() {
+ $mockee = 'lithium\console\command\Mock';
+ Mocker::create($mockee);
+ $this->assertTrue(class_exists($mockee));
+ }
+
+ public function testBasicCreationExtendsCorrectParent() {
+ $mocker = 'lithium\console\Request';
+ $mockeeObj = new \lithium\console\request\Mock();
+ $this->assertTrue(is_a($mockeeObj, $mocker));
+ }
+
+ public function testCannotMockNonLithiumClasses() {
+ $mockee = 'stdClass\Mock';
+ Mocker::create($mockee);
+ $this->assertTrue(!class_exists($mockee));
+ }
+
+ public function testCannotCreateNonStandardMockClass() {
+ $mockee = 'lithium\console\request\Mocker';
+ Mocker::create($mockee);
+ $this->assertTrue(!class_exists($mockee));
+ }
+
+ public function testFilteringNonStaticClass() {
+ $dispatcher = new \lithium\console\dispatcher\Mock();
+
+ $originalResult = $dispatcher->config();
+
+ $dispatcher->applyFilter('config', function($self, $params, $chain) {
+ return array();
+ });
+
+ $filteredResult = $dispatcher->config();
+
+ $this->assertEqual(0, count($filteredResult));
+ $this->assertNotEqual($filteredResult, $originalResult);
+ }
+
+ public function testFilteringNonStaticClassCanReturnOriginal() {
+ $response = new \lithium\console\response\Mock();
+
+ $originalResult = $response->styles();
+
+ $response->applyFilter('styles', function($self, $params, $chain) {
+ return $chain->next($self, $params, $chain);
+ });
+
+ $filteredResult = $response->styles();
+
+ $this->assertEqual($filteredResult, $originalResult);
+ }
+
+ public function testFilteringStaticClass() {
+ $mockee = 'lithium\analysis\parser\Mock';
+
+ $code = 'echo "foobar";';
+
+ $originalResult = $mockee::tokenize($code, array('wrap' => true));
+
+ $mockee::applyFilter('tokenize', function($self, $params, $chain) {
+ return array();
+ });
+
+ $filteredResult = $mockee::tokenize($code, array('wrap' => true));
+
+ $this->assertEqual(0, count($filteredResult));
+ $this->assertNotEqual($filteredResult, $originalResult);
+ }
+
+ public function testFilteringStaticClassCanReturnOriginal() {
+ $mockee = 'lithium\analysis\inspector\Mock';
+
+ $originalResult = $mockee::methods('lithium\analysis\Inspector');
+
+ $mockee::applyFilter('tokenize', function($self, $params, $chain) {
+ return $chain->next($self, $params, $chain);
+ });
+
+ $filteredResult = $mockee::methods('lithium\analysis\Inspector');
+
+ $this->assertEqual($filteredResult, $originalResult);
+ }
+
+}
+
+?>

0 comments on commit 195c6b8

Please sign in to comment.