LimeMock.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. <?php
  2. /*
  3. * This file is part of the Lime test framework.
  4. *
  5. * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
  6. * (c) Bernhard Schussek <bernhard.schussek@symfony-project.com>
  7. *
  8. * This source file is subject to the MIT license that is bundled
  9. * with this source code in the file LICENSE.
  10. */
  11. /**
  12. * Generates mock objects
  13. *
  14. * This class generates configurable mock objects based on existing interfaces,
  15. * classes or virtual (non-existing) class names. You can use it to create
  16. * objects of classes that you have not implemented yet, or to substitute
  17. * an existing class in a test.
  18. *
  19. * A mock object is created with the create() method:
  20. *
  21. * <code>
  22. * $mock = LimeMock::create('MyClass', $output);
  23. * </code>
  24. *
  25. * Note: The LimeTest class offers an easy access to preconfigured mocks and
  26. * stubs using the methods mock() and stub().
  27. *
  28. * Initially the mock is in recording mode. In this mode you just make the
  29. * expected method calls with the expected parameters. You can use modifiers
  30. * to configure return values or exceptions that should be thrown.
  31. *
  32. * <code>
  33. * // method "someMethod()" returns "return value" when called with "parameter"
  34. * $mock->someMethod('parameter')->returns('return value');
  35. * </code>
  36. *
  37. * You can find the complete list of method modifiers in class
  38. * LimeMockInvocationExpectation. By default, expected methods are initialized
  39. * with the modifier once(). If the option "nice" is set, the method is
  40. * initialized with the modifier any() instead.
  41. *
  42. * Once the recording is over, you must call the method replay() on the mock.
  43. * After the call to this method, the mock is in replay mode. In this mode, it
  44. * listens for method calls and returns the results configured before.
  45. *
  46. * <code>
  47. * $mock = LimeMock::create('MyClass', $output);
  48. * $mock->add(1, 2)->returns(3);
  49. * $mock->replay();
  50. *
  51. * echo $mock->add(1, 2);
  52. * // returns 3
  53. * </code>
  54. *
  55. * You also have the possibility to find out whether all the configured
  56. * methods have been called with the right parameters while in replay mode
  57. * by calling verify().
  58. *
  59. * <code>
  60. * $mock = LimeMock::create('MyClass', $output);
  61. * $mock->add(1,2);
  62. * $mock->replay();
  63. * $mock->add(1);
  64. * $mock->verify();
  65. *
  66. * // results in a failing test
  67. * </code>
  68. *
  69. * The method create() accepts several options to configure the created mock:
  70. *
  71. * * strict: If set to TRUE, the mock expects methods to be
  72. * called in the same order in which they were recorded.
  73. * Additionally, method parameters will be compared
  74. * with strict typing. Default: FALSE
  75. * * generate_controls: If set to FALSE, the mock's control methods
  76. * replay(), verify() etc. will not be generated.
  77. * Setting this option is useful when the mocked
  78. * class contains any of these methods. You then have
  79. * to access the control methods statically in this
  80. * class, f.i. LimeMock::replay($mock);
  81. * Default: TRUE
  82. * * stub_methods: If set to FALSE, method implementations in the
  83. * mocked class are called when a method is not
  84. * configured to be stubbed. Default: TRUE
  85. * * nice: See LimeMockBehaviour
  86. * * no_exceptions: See LimeMockBehaviour
  87. *
  88. * @package Lime
  89. * @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
  90. * @version SVN: $Id: LimeMock.php 24994 2009-12-06 21:02:45Z bschussek $
  91. * @see LimeMockBehaviour
  92. * @see LimeMockInvocationExpectation
  93. */
  94. class LimeMock
  95. {
  96. protected static
  97. $methodTemplate = '%s function %s(%s) { $args = func_get_args(); return $this->__call(\'%s\', $args); }',
  98. $parameterTemplate = '%s %s',
  99. $parameterWithDefaultTemplate = '%s %s = %s',
  100. $illegalMethods = array(
  101. '__construct',
  102. '__call',
  103. '__lime_replay',
  104. '__lime_getState',
  105. ),
  106. $controlMethods = array(
  107. 'replay',
  108. 'any',
  109. 'reset',
  110. 'verify',
  111. 'setExpectNothing',
  112. );
  113. /**
  114. * Creates a new mock object for the given class or interface name.
  115. *
  116. * The class/interface does not necessarily have to exist. Every generated
  117. * object fulfills the condition ($mock instanceof $class).
  118. *
  119. * @param string $classOrInterface The (non-)existing class/interface
  120. * you want to mock
  121. * @param LimeOutputInterface $output The output for displaying the test results
  122. * @param array $options Generation options. See the class
  123. * description for more information.
  124. * @return LimeMockInterface The mock object
  125. */
  126. public static function create($classOrInterface, LimeOutputInterface $output, array $options = array())
  127. {
  128. $options = array_merge(array(
  129. 'strict' => false,
  130. 'generate_controls' => true,
  131. 'stub_methods' => true,
  132. ), $options);
  133. if ($options['strict'])
  134. {
  135. $behaviour = new LimeMockOrderedBehaviour($options);
  136. }
  137. else
  138. {
  139. $behaviour = new LimeMockUnorderedBehaviour($options);
  140. }
  141. $name = self::generateClass($classOrInterface, $options['generate_controls']);
  142. return new $name($classOrInterface, $behaviour, $output, $options['stub_methods']);
  143. }
  144. /**
  145. * Generates a mock class for the given class/interface name and returns
  146. * the generated class name.
  147. *
  148. * @param string $classOrInterface The mocked class/interface name
  149. * @param boolean $generateControls Whether control methods should be generated.
  150. * @return string The generated class name
  151. */
  152. protected static function generateClass($classOrInterface, $generateControls = true)
  153. {
  154. $methods = '';
  155. if (!class_exists($classOrInterface, false) && !interface_exists($classOrInterface, false))
  156. {
  157. if (($pos = strpos($classOrInterface, '\\')) !== false)
  158. {
  159. $namespace = substr($classOrInterface, 0, $pos);
  160. $interface = substr($classOrInterface, $pos+1);
  161. eval(sprintf('namespace %s { interface %s {} }', $namespace, $interface));
  162. }
  163. else
  164. {
  165. eval(sprintf('interface %s {}', $classOrInterface));
  166. }
  167. }
  168. $class = new ReflectionClass($classOrInterface);
  169. foreach ($class->getMethods() as $method)
  170. {
  171. /* @var $method ReflectionMethod */
  172. if (in_array($method->getName(), self::$controlMethods) && $generateControls)
  173. {
  174. throw new LogicException(sprintf('The mocked class "%s" contains the method "%s", which conflicts with the mock\'s control methods. Please set the option "generate_controls" to false.', $classOrInterface, $method->getName()));
  175. }
  176. if (!in_array($method->getName(), self::$illegalMethods) && !$method->isFinal())
  177. {
  178. $modifiers = Reflection::getModifierNames($method->getModifiers());
  179. $modifiers = array_diff($modifiers, array('abstract'));
  180. $modifiers = implode(' ', $modifiers);
  181. $parameters = array();
  182. foreach ($method->getParameters() as $parameter)
  183. {
  184. $typeHint = '';
  185. /* @var $parameter ReflectionParameter */
  186. if ($parameter->getClass())
  187. {
  188. $typeHint = $parameter->getClass()->getName();
  189. }
  190. else if ($parameter->isArray())
  191. {
  192. $typeHint = 'array';
  193. }
  194. $name = '$'.$parameter->getName();
  195. if ($parameter->isPassedByReference())
  196. {
  197. $name = '&'.$name;
  198. }
  199. if ($parameter->isOptional())
  200. {
  201. $default = var_export($parameter->getDefaultValue(), true);
  202. $parameters[] = sprintf(self::$parameterWithDefaultTemplate, $typeHint, $name, $default);
  203. }
  204. else
  205. {
  206. $parameters[] = sprintf(self::$parameterTemplate, $typeHint, $name);
  207. }
  208. }
  209. $methods .= sprintf(self::$methodTemplate, $modifiers, $method->getName(),
  210. implode(', ', $parameters), $method->getName())."\n ";
  211. }
  212. }
  213. $interfaces = array();
  214. $name = self::generateName($class->getName());
  215. $declaration = 'class '.$name;
  216. if ($class->isInterface())
  217. {
  218. $interfaces[] = $class->getName();
  219. }
  220. else
  221. {
  222. $declaration .= ' extends '.$class->getName();
  223. }
  224. $interfaces[] = 'LimeMockInterface';
  225. if (count($interfaces) > 0)
  226. {
  227. $declaration .= ' implements '.implode(', ', $interfaces);
  228. }
  229. $template = new LimeMockTemplate(dirname(__FILE__).'/template/mocked_class.tpl');
  230. eval($template->render(array(
  231. 'class_declaration' => $declaration,
  232. 'methods' => $methods,
  233. 'generate_controls' => $generateControls,
  234. )));
  235. return $name;
  236. }
  237. /**
  238. * Generates a mock class name for the given original class/interface name.
  239. *
  240. * @param string $originalName
  241. * @return string
  242. */
  243. protected static function generateName($originalName)
  244. {
  245. // strip namespace separators
  246. $originalName = str_replace('\\', '_', $originalName);
  247. while (!isset($name) || class_exists($name, false))
  248. {
  249. // inspired by PHPUnit_Framework_MockObject_Generator
  250. $name = 'Mock_'.$originalName.'_'.substr(md5(microtime()), 0, 8);
  251. }
  252. return $name;
  253. }
  254. /**
  255. * Turns the given mock into replay mode.
  256. *
  257. * @param LimeMockInterface $mock
  258. */
  259. public static function replay(LimeMockInterface $mock)
  260. {
  261. return $mock->__lime_replay();
  262. }
  263. /**
  264. * Resets the given mock.
  265. *
  266. * All expected invocations are removed, the mock is set to record mode again.
  267. *
  268. * @param LimeMockInterface $mock
  269. */
  270. public static function reset(LimeMockInterface $mock)
  271. {
  272. return $mock->__lime_reset();
  273. }
  274. /**
  275. * Expects the given method on the given mock to be called with any parameters.
  276. *
  277. * The LimeMockInvocationExpectation object is returned and allows you to
  278. * set further modifiers on the method expectation.
  279. *
  280. * @param LimeMockInterface $mock
  281. * @param string $methodName
  282. * @return LimeMockInvocationExpectation
  283. */
  284. public static function any(LimeMockInterface $mock, $methodName)
  285. {
  286. return $mock->__call($methodName, null);
  287. }
  288. /**
  289. * Configures the mock to expect no method call.
  290. *
  291. * @param LimeMockInterface $mock
  292. */
  293. public static function setExpectNothing(LimeMockInterface $mock)
  294. {
  295. return $mock->__lime_getState()->setExpectNothing();
  296. }
  297. /**
  298. * Verifies the given mock.
  299. *
  300. * @param LimeMockInterface $mock
  301. */
  302. public static function verify(LimeMockInterface $mock)
  303. {
  304. return $mock->__lime_getState()->verify();
  305. }
  306. }