Skip to content
This repository

Add a lithium mocking class with an autoloader. #755

Merged
merged 1 commit into from over 1 year ago

2 participants

Blaine Schmeisser Nate Abele
Blaine Schmeisser
Collaborator

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%

Blaine Schmeisser 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%
195c6b8
Blaine Schmeisser
Collaborator

Fix for #750

Nate Abele
Owner

You didn't say "I do not like them, Sam I am", but okay.

Nate Abele nateabele merged commit 5975540 into from December 19, 2012
Nate Abele nateabele closed this December 19, 2012
Blaine Schmeisser
Collaborator

I was hoping there would be a little more discussion about this. I'll try and write up a better class doc tonight or tomorrow with a few examples.

I thought of an issue with this and I can think of two possible solutions. The issue is that because $results is generated -before- the filter it's possible the results could manipulate the class. At which point your filter and the original method are both ran and we edit the same thing twice. Your filter may simply overwrite what the default method does, or both may add to an array.

We -could- attempt to open the file up, copy/paste in some lines from the method, making sure the method has a $that reference and doing a string replace. This seems kinda messy and we still run into an issue with trying to access protected/private methods.

Another idea, is to just overwrite the default visibility of methods, since in PHP you can always make a method more visible, we could just force all methods to be public in the mock and move the call from out of the filter to inside of the filter.

Thoughts?

Blaine Schmeisser
Collaborator

I created a unique 2 parent system.

First extend the class and make all method public and add some logic to the methods.

Second we extend that and overwrite the constructor, and create an instance of the object first step class, we recreate all methods and make them filterable and call the stored mock.

Back on the first class we check to see who's calling who. If the second step mock is calling it we pass it along, if not we send it through the second step mock.

The only downside I see to doing it this way would be that the base class that we mock needs to call "static" instead of "self" which should be happening anyways if you ever plan on extending it.

If I don't make sense I made an example gist here. I'm not sure why the formatting is messed up but it's fine if you view raw.

Nate Abele
Owner

I was hoping there would be a little more discussion about this [...] I thought of an issue with this and I can think of two possible solutions.

We're in between releases, and you implemented something decent where we currently have nothing. It's okay to start somewhere and improve incrementally, so long as it's clean by the time we release.

The only downside I see to doing it this way would be that the base class that we mock needs to call "static" instead of "self" which should be happening anyways if you ever plan on extending it.

We only have a couple of calls to self in the framework, and always advocate static over it. I don't see why this should be a problem.

Blaine Schmeisser
Collaborator

I almost have something to commit on top of this with some docs on how to use it and a few more unit tests to cover the patch.

When is the 0.12 release scheduled? This patch should be submitted within an hour or so unless something goes horribly wrong (like the end of the world).

Nate Abele
Owner

Assuming no end-of-the-world scenarios, the next release is planned for a few weeks from now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 1 unique commit by 1 author.

Dec 19, 2012
Blaine Schmeisser 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%
195c6b8
This page is out of date. Refresh to see the latest.
212  test/Mocker.php
... ...
@@ -0,0 +1,212 @@
  1
+<?php
  2
+/**
  3
+ * Lithium: the most rad php framework
  4
+ *
  5
+ * @copyright     Copyright 2012, Union of RAD (http://union-of-rad.org)
  6
+ * @license       http://opensource.org/licenses/bsd-license.php The BSD License
  7
+ */
  8
+
  9
+namespace lithium\test;
  10
+
  11
+use lithium\util\String;
  12
+use ReflectionClass;
  13
+use ReflectionMethod;
  14
+use Reflection;
  15
+
  16
+/**
  17
+ * The Mocker class aids in the creation of Mocks on the fly.
  18
+ */
  19
+class Mocker {
  20
+
  21
+	/**
  22
+	 * A list of code to be generated based on the type.
  23
+	 *
  24
+	 * @var array
  25
+	 */
  26
+	protected static $_dynamicCode = array(
  27
+		'startClass' => array(
  28
+			'namespace {:namespace};',
  29
+			'class {:mockee} extends \{:mocker} {'
  30
+		),
  31
+		'staticMethod' => array(
  32
+			'{:modifiers} function {:name}({:params}) {',
  33
+			'    $params = func_get_args();',
  34
+			'    list($class, $method) = explode(\'::\', __METHOD__, 2);',
  35
+			'    $parent = \'parent::\' . $method;',
  36
+			'    $result = call_user_func_array($parent, $params);',
  37
+			'    return self::_filter($method, $params, function($self, $params) use(&$result) {',
  38
+			'       return $result;',
  39
+			'    });',
  40
+			'}',
  41
+		),
  42
+		'method' => array(
  43
+			'{:modifiers} function {:name}({:params}) {',
  44
+			'    $params = func_get_args();',
  45
+			'    list($class, $method) = explode(\'::\', __METHOD__, 2);',
  46
+			'    $parent = \'parent::\' . $method;',
  47
+			'    $result = call_user_func_array($parent, $params);',
  48
+			'    return $this->_filter($parent, $params, function($self, $params) use(&$result) {',
  49
+			'        return $result;',
  50
+			'    });',
  51
+			'}',
  52
+		),
  53
+		'endClass' => array(
  54
+			'}',
  55
+		),
  56
+	);
  57
+
  58
+	/**
  59
+	 * A list of methods we should not overwrite in our mock class.
  60
+	 *
  61
+	 * @var array
  62
+	 */
  63
+	protected static $_blackList = array(
  64
+		'__construct', '__destruct', '__call', '__callStatic',
  65
+		'__get', '__set', '__isset', '__unset', '__sleep',
  66
+		'__wakeup', '__toString', '__clone', '__invoke',
  67
+		'__construct', '_init', 'applyFilter', 'invokeMethod',
  68
+		'__set_state', '_instance', '_filter', '_parents',
  69
+		'_stop',
  70
+	);
  71
+
  72
+	/**
  73
+	 * Will register this class into the autoloader.
  74
+	 *
  75
+	 * @return void
  76
+	 */
  77
+	public static function register() {
  78
+		spl_autoload_register(array(__CLASS__, 'create'));
  79
+	}
  80
+
  81
+	/**
  82
+	 * The main entrance to create a new Mock class.
  83
+	 *
  84
+	 * @param  string $mockee The fully namespaced `\Mock` class
  85
+	 * @return void
  86
+	 */
  87
+	public static function create($mockee) {
  88
+		if (!self::_validateMockee($mockee)) {
  89
+			return;
  90
+		}
  91
+
  92
+		$mocker = self::_mocker($mockee);
  93
+
  94
+		$code = self::_dynamicCode('startClass', array(
  95
+			'namespace' => self::_namespace($mockee),
  96
+			'mocker' => $mocker,
  97
+			'mockee' => 'Mock',
  98
+		));
  99
+
  100
+		$reflectedClass = new ReflectionClass($mocker);
  101
+		$reflecedMethods = $reflectedClass->getMethods();
  102
+		foreach ($reflecedMethods as $method) {
  103
+			if (!in_array($method->name, self::$_blackList)) {
  104
+				$key = $method->isStatic() ? 'staticMethod' : 'method';
  105
+				$code .= self::_dynamicCode($key, array(
  106
+					'name' => $method->name,
  107
+					'modifiers' => self::_methodModifiers($method),
  108
+					'params' => self::_methodParams($method),
  109
+					'visibility' => 'public',
  110
+				));
  111
+			}
  112
+		}
  113
+
  114
+		$code .= self::_dynamicCode('endClass');
  115
+
  116
+		eval($code);
  117
+	}
  118
+
  119
+	/**
  120
+	 * Will determine what method mofifiers of a method.
  121
+	 *
  122
+	 * For instance: 'public static' or 'private abstract'
  123
+	 *
  124
+	 * @param  ReflectionMethod $method
  125
+	 * @return string
  126
+	 */
  127
+	protected static function _methodModifiers(ReflectionMethod $method) {
  128
+		$modifierKey = $method->getModifiers();
  129
+		$modifierArray = Reflection::getModifierNames($modifierKey);
  130
+		return implode(' ', $modifierArray);
  131
+	}
  132
+
  133
+	/**
  134
+	 * Will determine what parameter prototype of a method.
  135
+	 *
  136
+	 * For instance: 'ReflectionMethod $method' or '$name, array $foo = null'
  137
+	 *
  138
+	 * @param  ReflectionMethod $method
  139
+	 * @return string
  140
+	 */
  141
+	protected static function _methodParams(ReflectionMethod $method) {
  142
+		$pattern = '/Parameter #[0-9]+ \[ [^\>]+>([^\]]+) \]/';
  143
+		$replace = array(
  144
+			'from' => array('Array', 'or NULL'),
  145
+			'to' => array('array()', ''),
  146
+		);
  147
+		preg_match_all($pattern, $method, $matches);
  148
+		$params = implode(', ', $matches[1]);
  149
+		return str_replace($replace['from'], $replace['to'], $params);
  150
+	}
  151
+
  152
+	/**
  153
+	 * Will generate the code you are wanting.
  154
+	 *
  155
+	 * @param  string $key    The key from self::$_dynamicCode
  156
+	 * @param  array  $tokens Tokens, if any, that should be inserted
  157
+	 * @return string
  158
+	 */
  159
+	protected static function _dynamicCode($key, $tokens = array()) {
  160
+		$code = implode("\n", self::$_dynamicCode[$key]);
  161
+		return String::insert($code, $tokens) . "\n";
  162
+	}
  163
+
  164
+	/**
  165
+	 * Will generate the mocker from the current mockee.
  166
+	 *
  167
+	 * @param  string $mockee The fully namespaced `\Mock` class
  168
+	 * @return array
  169
+	 */
  170
+	protected static function _mocker($mockee) {
  171
+		$matches = array();
  172
+		preg_match_all('/^(.*)\\\\([^\\\\]+)\\\\Mock$/', $mockee, $matches);
  173
+		if (!isset($matches[1][0])) {
  174
+			return;
  175
+		}
  176
+		return $matches[1][0] . '\\' . ucfirst($matches[2][0]);
  177
+	}
  178
+
  179
+	/**
  180
+	 * Will generate the namespace from the current mockee.
  181
+	 *
  182
+	 * @param  string $mockee The fully namespaced `\Mock` class
  183
+	 * @return string
  184
+	 */
  185
+	protected static function _namespace($mockee) {
  186
+		$matches = array();
  187
+		preg_match_all('/^(.*)\\\\Mock$/', $mockee, $matches);
  188
+		return isset($matches[1][0]) ? $matches[1][0] : null;
  189
+	}
  190
+
  191
+	/**
  192
+	 * Will validate if mockee is a valid class we should mock.
  193
+	 *
  194
+	 * @param  string $mockee The fully namespaced `\Mock` class
  195
+	 * @return bool
  196
+	 */
  197
+	protected static function _validateMockee($mockee) {
  198
+		if (class_exists($mockee) || preg_match('/\\\\Mock$/', $mockee) !== 1) {
  199
+			return false;
  200
+		}
  201
+		$mocker = self::_mocker($mockee);
  202
+		$isObject = is_subclass_of($mocker, 'lithium\core\Object');
  203
+		$isStatic = is_subclass_of($mocker, 'lithium\core\StaticObject');
  204
+		if (!$isObject && !$isStatic) {
  205
+			return false;
  206
+		}
  207
+		return true;
  208
+	}
  209
+
  210
+}
  211
+
  212
+?>
118  tests/cases/test/MockerTest.php
... ...
@@ -0,0 +1,118 @@
  1
+<?php
  2
+/**
  3
+ * Lithium: the most rad php framework
  4
+ *
  5
+ * @copyright     Copyright 2012, Union of RAD (http://union-of-rad.org)
  6
+ * @license       http://opensource.org/licenses/bsd-license.php The BSD License
  7
+ */
  8
+
  9
+namespace lithium\tests\cases\test;
  10
+
  11
+use lithium\test\Mocker;
  12
+
  13
+/**
  14
+ * WARNING:
  15
+ * No unit test should mock the same test as another
  16
+ */
  17
+class MockerTest extends \lithium\test\Unit {
  18
+
  19
+	public function setUp() {
  20
+		Mocker::register();
  21
+	}
  22
+
  23
+	public function testAutoloadRegister() {
  24
+		Mocker::register();
  25
+		$registered = spl_autoload_functions();
  26
+		$this->assertTrue(in_array(array(
  27
+			'lithium\test\Mocker',
  28
+			'create'
  29
+		), $registered));
  30
+	}
  31
+
  32
+	public function testBasicCreation() {
  33
+		$mockee = 'lithium\console\command\Mock';
  34
+		Mocker::create($mockee);
  35
+		$this->assertTrue(class_exists($mockee));
  36
+	}
  37
+
  38
+	public function testBasicCreationExtendsCorrectParent() {
  39
+		$mocker = 'lithium\console\Request';
  40
+		$mockeeObj = new \lithium\console\request\Mock();
  41
+		$this->assertTrue(is_a($mockeeObj, $mocker));
  42
+	}
  43
+
  44
+	public function testCannotMockNonLithiumClasses() {
  45
+		$mockee = 'stdClass\Mock';
  46
+		Mocker::create($mockee);
  47
+		$this->assertTrue(!class_exists($mockee));
  48
+	}
  49
+
  50
+	public function testCannotCreateNonStandardMockClass() {
  51
+		$mockee = 'lithium\console\request\Mocker';
  52
+		Mocker::create($mockee);
  53
+		$this->assertTrue(!class_exists($mockee));
  54
+	}
  55
+
  56
+	public function testFilteringNonStaticClass() {
  57
+		$dispatcher = new \lithium\console\dispatcher\Mock();
  58
+		
  59
+		$originalResult = $dispatcher->config();
  60
+
  61
+		$dispatcher->applyFilter('config', function($self, $params, $chain) {
  62
+			return array();
  63
+		});
  64
+
  65
+		$filteredResult = $dispatcher->config();
  66
+
  67
+		$this->assertEqual(0, count($filteredResult));
  68
+		$this->assertNotEqual($filteredResult, $originalResult);
  69
+	}
  70
+
  71
+	public function testFilteringNonStaticClassCanReturnOriginal() {
  72
+		$response = new \lithium\console\response\Mock();
  73
+		
  74
+		$originalResult = $response->styles();
  75
+
  76
+		$response->applyFilter('styles', function($self, $params, $chain) {
  77
+			return $chain->next($self, $params, $chain);
  78
+		});
  79
+
  80
+		$filteredResult = $response->styles();
  81
+
  82
+		$this->assertEqual($filteredResult, $originalResult);
  83
+	}
  84
+
  85
+	public function testFilteringStaticClass() {
  86
+		$mockee = 'lithium\analysis\parser\Mock';
  87
+
  88
+		$code = 'echo "foobar";';
  89
+		
  90
+		$originalResult = $mockee::tokenize($code, array('wrap' => true));
  91
+
  92
+		$mockee::applyFilter('tokenize', function($self, $params, $chain) {
  93
+			return array();
  94
+		});
  95
+
  96
+		$filteredResult = $mockee::tokenize($code, array('wrap' => true));
  97
+
  98
+		$this->assertEqual(0, count($filteredResult));
  99
+		$this->assertNotEqual($filteredResult, $originalResult);
  100
+	}
  101
+
  102
+	public function testFilteringStaticClassCanReturnOriginal() {
  103
+		$mockee = 'lithium\analysis\inspector\Mock';
  104
+		
  105
+		$originalResult = $mockee::methods('lithium\analysis\Inspector');
  106
+
  107
+		$mockee::applyFilter('tokenize', function($self, $params, $chain) {
  108
+			return $chain->next($self, $params, $chain);
  109
+		});
  110
+
  111
+		$filteredResult = $mockee::methods('lithium\analysis\Inspector');
  112
+
  113
+		$this->assertEqual($filteredResult, $originalResult);
  114
+	}
  115
+
  116
+}
  117
+
  118
+?>
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.