PageRenderTime 51ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Codeception/Util/Stub.php

https://github.com/pmcjury/Codeception
PHP | 686 lines | 386 code | 38 blank | 262 comment | 13 complexity | 4d73854144832a20c442ac804f1db1c8 MD5 | raw file
  1. <?php
  2. namespace Codeception\Util;
  3. class Stub
  4. {
  5. public static $magicMethods = ['__isset', '__get', '__set'];
  6. /**
  7. * Instantiates a class without executing a constructor.
  8. * Properties and methods can be set as a second parameter.
  9. * Even protected and private properties can be set.
  10. *
  11. * ``` php
  12. * <?php
  13. * Stub::make('User');
  14. * Stub::make('User', array('name' => 'davert));
  15. * ?>
  16. * ```
  17. *
  18. * Accepts either name of class or object of that class
  19. *
  20. * ``` php
  21. * <?php
  22. * Stub::make(new User, array('name' => 'davert));
  23. * ?>
  24. * ```
  25. *
  26. * To replace method provide it's name as a key in second parameter and it's return value or callback function as parameter
  27. *
  28. * ``` php
  29. * <?php
  30. * Stub::make('User', array('save' => function () { return true; }));
  31. * Stub::make('User', array('save' => true }));
  32. * ?>
  33. * ```
  34. *
  35. * @param $class - A class to be mocked
  36. * @param array $params - properties and methods to set
  37. * @param bool|\PHPUnit_Framework_TestCase $testCase
  38. *
  39. * @return object - mock
  40. * @throws \RuntimeException when class not exists
  41. */
  42. public static function make($class, $params = [], $testCase = false)
  43. {
  44. $class = self::getClassname($class);
  45. if (!class_exists($class)) {
  46. throw new \RuntimeException("Stubbed class $class doesn't exist.");
  47. }
  48. $reflection = new \ReflectionClass($class);
  49. $callables = self::getMethodsToReplace($reflection, $params);
  50. if ($reflection->isAbstract()) {
  51. $arguments = empty($callables) ? [] : array_keys($callables);
  52. $mock = self::generateMockForAbstractClass($class, $arguments, '', false, $testCase);
  53. } else {
  54. $arguments = empty($callables) ? null : array_keys($callables);
  55. $mock = self::generateMock($class, $arguments, [], '', false, $testCase);
  56. }
  57. self::bindParameters($mock, $params);
  58. $mock->__mocked = $class;
  59. return $mock;
  60. }
  61. /**
  62. * Creates $num instances of class through `Stub::make`.
  63. *
  64. * @param $class
  65. * @param int $num
  66. * @param array $params
  67. *
  68. * @return array
  69. */
  70. public static function factory($class, $num = 1, $params = [])
  71. {
  72. $objects = [];
  73. for ($i = 0; $i < $num; $i++) {
  74. $objects[] = self::make($class, $params);
  75. }
  76. return $objects;
  77. }
  78. /**
  79. * Instantiates class having all methods replaced with dummies except one.
  80. * Constructor is not triggered.
  81. * Properties and methods can be replaced.
  82. * Even protected and private properties can be set.
  83. *
  84. * ``` php
  85. * <?php
  86. * Stub::makeEmptyExcept('User', 'save');
  87. * Stub::makeEmptyExcept('User', 'save', array('name' => 'davert'));
  88. * ?>
  89. * ```
  90. *
  91. * Accepts either name of class or object of that class
  92. *
  93. * ``` php
  94. * <?php
  95. * * Stub::makeEmptyExcept(new User, 'save');
  96. * ?>
  97. * ```
  98. *
  99. * To replace method provide it's name as a key in second parameter and it's return value or callback function as parameter
  100. *
  101. * ``` php
  102. * <?php
  103. * Stub::makeEmptyExcept('User', 'save', array('isValid' => function () { return true; }));
  104. * Stub::makeEmptyExcept('User', 'save', array('isValid' => true }));
  105. * ?>
  106. * ```
  107. *
  108. * @param $class
  109. * @param $method
  110. * @param array $params
  111. * @param bool|\PHPUnit_Framework_TestCase $testCase
  112. *
  113. * @return object
  114. */
  115. public static function makeEmptyExcept($class, $method, $params = [], $testCase = false)
  116. {
  117. $class = self::getClassname($class);
  118. $reflectionClass = new \ReflectionClass($class);
  119. $methods = $reflectionClass->getMethods();
  120. $methods = array_filter(
  121. $methods,
  122. function ($m) {
  123. return !in_array($m->name, Stub::$magicMethods);
  124. }
  125. );
  126. $methods = array_filter(
  127. $methods,
  128. function ($m) use ($method) {
  129. return $method != $m->name;
  130. }
  131. );
  132. $methods = array_map(
  133. function ($m) {
  134. return $m->name;
  135. },
  136. $methods
  137. );
  138. $methods = count($methods) ? $methods : null;
  139. $mock = self::generateMock($class, $methods, array(), '', false, $testCase);
  140. self::bindParameters($mock, $params);
  141. $mock->__mocked = $class;
  142. return $mock;
  143. }
  144. /**
  145. * Instantiates class having all methods replaced with dummies.
  146. * Constructor is not triggered.
  147. * Properties and methods can be set as a second parameter.
  148. * Even protected and private properties can be set.
  149. *
  150. * ``` php
  151. * <?php
  152. * Stub::makeEmpty('User');
  153. * Stub::makeEmpty('User', array('name' => 'davert));
  154. * ?>
  155. * ```
  156. *
  157. * Accepts either name of class or object of that class
  158. *
  159. * ``` php
  160. * <?php
  161. * Stub::makeEmpty(new User, array('name' => 'davert));
  162. * ?>
  163. * ```
  164. *
  165. * To replace method provide it's name as a key in second parameter and it's return value or callback function as parameter
  166. *
  167. * ``` php
  168. * <?php
  169. * Stub::makeEmpty('User', array('save' => function () { return true; }));
  170. * Stub::makeEmpty('User', array('save' => true }));
  171. * ?>
  172. * ```
  173. *
  174. * @param $class
  175. * @param array $params
  176. * @param bool|\PHPUnit_Framework_TestCase $testCase
  177. *
  178. * @return object
  179. */
  180. public static function makeEmpty($class, $params = array(), $testCase = false)
  181. {
  182. $class = self::getClassname($class);
  183. $methods = get_class_methods($class);
  184. $methods = array_filter(
  185. $methods,
  186. function ($i) {
  187. return !in_array($i, Stub::$magicMethods);
  188. }
  189. );
  190. $mock = self::generateMock($class, $methods, array(), '', false, $testCase);
  191. self::bindParameters($mock, $params);
  192. $mock->__mocked = $class;
  193. return $mock;
  194. }
  195. /**
  196. * Clones an object and redefines it's properties (even protected and private)
  197. *
  198. * @param $obj
  199. * @param array $params
  200. *
  201. * @return mixed
  202. */
  203. public static function copy($obj, $params = array())
  204. {
  205. $copy = clone($obj);
  206. self::bindParameters($copy, $params);
  207. return $copy;
  208. }
  209. /**
  210. * Instantiates a class instance by running constructor.
  211. * Parameters for constructor passed as second argument
  212. * Properties and methods can be set in third argument.
  213. * Even protected and private properties can be set.
  214. *
  215. * ``` php
  216. * <?php
  217. * Stub::construct('User', array('autosave' => false));
  218. * Stub::construct('User', array('autosave' => false), array('name' => 'davert));
  219. * ?>
  220. * ```
  221. *
  222. * Accepts either name of class or object of that class
  223. *
  224. * ``` php
  225. * <?php
  226. * Stub::construct(new User, array('autosave' => false), array('name' => 'davert));
  227. * ?>
  228. * ```
  229. *
  230. * To replace method provide it's name as a key in second parameter and it's return value or callback function as parameter
  231. *
  232. * ``` php
  233. * <?php
  234. * Stub::construct('User', array(), array('save' => function () { return true; }));
  235. * Stub::construct('User', array(), array('save' => true }));
  236. * ?>
  237. * ```
  238. *
  239. * @param $class
  240. * @param array $constructorParams
  241. * @param array $params
  242. * @param bool|\PHPUnit_Framework_TestCase $testCase
  243. *
  244. * @return object
  245. */
  246. public static function construct($class, $constructorParams = array(), $params = array(), $testCase = false)
  247. {
  248. $class = self::getClassname($class);
  249. $callables = self::getMethodsToReplace(new \ReflectionClass($class), $params);
  250. $arguments = empty($callables) ? null : array_keys($callables);
  251. $mock = self::generateMock($class, $arguments, $constructorParams, $testCase);
  252. self::bindParameters($mock, $params);
  253. $mock->__mocked = $class;
  254. return $mock;
  255. }
  256. /**
  257. * Instantiates a class instance by running constructor with all methods replaced with dummies.
  258. * Parameters for constructor passed as second argument
  259. * Properties and methods can be set in third argument.
  260. * Even protected and private properties can be set.
  261. *
  262. * ``` php
  263. * <?php
  264. * Stub::constructEmpty('User', array('autosave' => false));
  265. * Stub::constructEmpty('User', array('autosave' => false), array('name' => 'davert));
  266. * ?>
  267. * ```
  268. *
  269. * Accepts either name of class or object of that class
  270. *
  271. * ``` php
  272. * <?php
  273. * Stub::constructEmpty(new User, array('autosave' => false), array('name' => 'davert));
  274. * ?>
  275. * ```
  276. *
  277. * To replace method provide it's name as a key in second parameter and it's return value or callback function as parameter
  278. *
  279. * ``` php
  280. * <?php
  281. * Stub::constructEmpty('User', array(), array('save' => function () { return true; }));
  282. * Stub::constructEmpty('User', array(), array('save' => true }));
  283. * ?>
  284. * ```
  285. *
  286. * @param $class
  287. * @param array $constructorParams
  288. * @param array $params
  289. * @param bool|\PHPUnit_Framework_TestCase $testCase
  290. *
  291. * @return object
  292. */
  293. public static function constructEmpty($class, $constructorParams = array(), $params = array(), $testCase = false)
  294. {
  295. $class = self::getClassname($class);
  296. $methods = get_class_methods($class);
  297. $methods = array_filter(
  298. $methods,
  299. function ($i) {
  300. return !in_array($i, Stub::$magicMethods);
  301. }
  302. );
  303. $mock = self::generateMock($class, $methods, $constructorParams, $testCase);
  304. self::bindParameters($mock, $params);
  305. $mock->__mocked = $class;
  306. return $mock;
  307. }
  308. /**
  309. * Instantiates a class instance by running constructor with all methods replaced with dummies, except one.
  310. * Parameters for constructor passed as second argument
  311. * Properties and methods can be set in third argument.
  312. * Even protected and private properties can be set.
  313. *
  314. * ``` php
  315. * <?php
  316. * Stub::constructEmptyExcept('User', 'save');
  317. * Stub::constructEmptyExcept('User', 'save', array('autosave' => false), array('name' => 'davert));
  318. * ?>
  319. * ```
  320. *
  321. * Accepts either name of class or object of that class
  322. *
  323. * ``` php
  324. * <?php
  325. * Stub::constructEmptyExcept(new User, 'save', array('autosave' => false), array('name' => 'davert));
  326. * ?>
  327. * ```
  328. *
  329. * To replace method provide it's name as a key in second parameter and it's return value or callback function as parameter
  330. *
  331. * ``` php
  332. * <?php
  333. * Stub::constructEmptyExcept('User', 'save', array(), array('save' => function () { return true; }));
  334. * Stub::constructEmptyExcept('User', 'save', array(), array('save' => true }));
  335. * ?>
  336. * ```
  337. *
  338. * @param $class
  339. * @param $method
  340. * @param array $constructorParams
  341. * @param array $params
  342. * @param bool|PHPUnit_Framework_TestCase $testCase
  343. *
  344. * @return object
  345. */
  346. public static function constructEmptyExcept(
  347. $class,
  348. $method,
  349. $constructorParams = array(),
  350. $params = array(),
  351. $testCase = false
  352. ) {
  353. $class = self::getClassname($class);
  354. $reflectionClass = new \ReflectionClass($class);
  355. $methods = $reflectionClass->getMethods();
  356. $methods = array_filter(
  357. $methods,
  358. function ($m) {
  359. return !in_array($m->name, Stub::$magicMethods);
  360. }
  361. );
  362. $methods = array_filter(
  363. $methods,
  364. function ($m) use ($method) {
  365. return $method != $m->name;
  366. }
  367. );
  368. $methods = array_map(
  369. function ($m) {
  370. return $m->name;
  371. },
  372. $methods
  373. );
  374. $methods = count($methods) ? $methods : null;
  375. $mock = self::generateMock($class, $methods, $constructorParams, $testCase);
  376. self::bindParameters($mock, $params);
  377. $mock->__mocked = $class;
  378. return $mock;
  379. }
  380. private static function generateMock()
  381. {
  382. return self::doGenerateMock(func_get_args());
  383. }
  384. /**
  385. * Returns a mock object for the specified abstract class with all abstract
  386. * methods of the class mocked. Concrete methods to mock can be specified with
  387. * the last parameter
  388. *
  389. * @param string $originalClassName
  390. * @param array $arguments
  391. * @param string $mockClassName
  392. * @param boolean $callOriginalConstructor
  393. * @param boolean $callOriginalClone
  394. * @param boolean $callAutoload
  395. * @param array $mockedMethods
  396. * @param boolean $cloneArguments
  397. *
  398. * @return object
  399. * @since Method available since Release 1.0.0
  400. * @throws InvalidArgumentException
  401. */
  402. private static function generateMockForAbstractClass()
  403. {
  404. return self::doGenerateMock(func_get_args(), true);
  405. }
  406. private static function doGenerateMock($args, $isAbstract = false)
  407. {
  408. $testCase = self::extractTestCaseFromArgs($args);
  409. $class = $testCase instanceof \PHPUnit_Framework_TestCase ? $testCase : new \PHPUnit_Framework_MockObject_Generator;
  410. $methodName = $isAbstract ? 'getMockForAbstractClass' : 'getMock';
  411. $mock = call_user_func_array([$class, $methodName], $args);
  412. return $mock;
  413. }
  414. private static function extractTestCaseFromArgs(&$args)
  415. {
  416. $argsLength = count($args) - 1;
  417. $testCase = $args[$argsLength];
  418. unset($args[$argsLength]);
  419. return $testCase;
  420. }
  421. /**
  422. * Replaces properties and methods of current stub
  423. *
  424. * @param \PHPUnit_Framework_MockObject_MockObject $mock
  425. * @param array $params
  426. *
  427. * @return mixed
  428. * @throws \LogicException
  429. */
  430. public static function update($mock, array $params)
  431. {
  432. if (!$mock->__mocked) {
  433. throw new \LogicException('You can update only stubbed objects');
  434. }
  435. self::bindParameters($mock, $params);
  436. return $mock;
  437. }
  438. /**
  439. * @param \PHPUnit_Framework_MockObject_MockObject $mock
  440. * @param array $params
  441. */
  442. protected static function bindParameters($mock, $params)
  443. {
  444. $reflectionClass = new \ReflectionClass($mock);
  445. foreach ($params as $param => $value) {
  446. // redefine method
  447. if ($reflectionClass->hasMethod($param)) {
  448. if ($value instanceof StubMarshaler) {
  449. $marshaler = $value;
  450. $mock->
  451. expects($marshaler->getMatcher())->
  452. method($param)->
  453. will(new \PHPUnit_Framework_MockObject_Stub_ReturnCallback($marshaler->getValue()));
  454. } elseif ($value instanceof \Closure) {
  455. $mock->
  456. expects(new \PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount)->
  457. method($param)->
  458. will(new \PHPUnit_Framework_MockObject_Stub_ReturnCallback($value));
  459. } else {
  460. $mock->
  461. expects(new \PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount)->
  462. method($param)->
  463. will(new \PHPUnit_Framework_MockObject_Stub_Return($value));
  464. }
  465. } elseif ($reflectionClass->hasProperty($param)) {
  466. $reflectionProperty = $reflectionClass->getProperty($param);
  467. $reflectionProperty->setAccessible(true);
  468. $reflectionProperty->setValue($mock, $value);
  469. continue;
  470. } else {
  471. $mock->$param = $value;
  472. continue;
  473. }
  474. }
  475. }
  476. /**
  477. * @todo should be simplified
  478. */
  479. protected static function getClassname($object)
  480. {
  481. if (is_object($object)) {
  482. return get_class($object);
  483. }
  484. if (is_callable($object)) {
  485. return call_user_func($object);
  486. }
  487. return $object;
  488. }
  489. protected static function getMethodsToReplace($reflection, $params)
  490. {
  491. $callables = array();
  492. foreach ($params as $method => $value) {
  493. if ($reflection->hasMethod($method)) {
  494. $callables[$method] = $value;
  495. }
  496. }
  497. return $callables;
  498. }
  499. /**
  500. * Checks if a method never has been invoked
  501. *
  502. * If method invoked, it will immediately throw an
  503. * exception.
  504. *
  505. * ``` php
  506. * <?php
  507. * $user = Stub::make('User', array('getName' => Stub::never(), 'someMethod' => function() {}));
  508. * $user->someMethod();
  509. * ?>
  510. * ```
  511. *
  512. * @param mixed $params
  513. *
  514. * @return StubMarshaler
  515. */
  516. public static function never($params = null)
  517. {
  518. return new StubMarshaler(
  519. new \PHPUnit_Framework_MockObject_Matcher_InvokedCount(0),
  520. self::closureIfNull($params)
  521. );
  522. }
  523. /**
  524. * Checks if a method has been invoked exactly one
  525. * time.
  526. *
  527. * If the number is less or greater it will later be checked in verify() and also throw an
  528. * exception.
  529. *
  530. * ``` php
  531. * <?php
  532. * $user = Stub::make('User', array('getName' => Stub::once(function() { return 'Davert';}), 'someMethod' => function() {}));
  533. * $userName = $user->getName();
  534. * $this->assertEquals('Davert', $userName);
  535. * ?>
  536. * ```
  537. *
  538. * @param mixed $params
  539. *
  540. * @return StubMarshaler
  541. */
  542. public static function once($params = null)
  543. {
  544. return new StubMarshaler(
  545. new \PHPUnit_Framework_MockObject_Matcher_InvokedCount(1),
  546. self::closureIfNull($params)
  547. );
  548. }
  549. /**
  550. * Checks if a method has been invoked at least one
  551. * time.
  552. *
  553. * If the number of invocations is 0 it will throw an exception in verify.
  554. *
  555. * ``` php
  556. * <?php
  557. * $user = Stub::make('User', array('getName' => Stub::atLeastOnce(function() { return 'Davert';}), 'someMethod' => function() {}));
  558. * $user->getName();
  559. * $user->getName();
  560. * ?>
  561. * ```
  562. *
  563. * @param mixed $params
  564. *
  565. * @return StubMarshaler
  566. */
  567. public static function atLeastOnce($params = null)
  568. {
  569. return new StubMarshaler(
  570. new \PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastOnce,
  571. self::closureIfNull($params)
  572. );
  573. }
  574. /**
  575. * Checks if a method has been invoked a certain amount
  576. * of times.
  577. * If the number of invocations exceeds the value it will immediately throw an
  578. * exception,
  579. * If the number is less it will later be checked in verify() and also throw an
  580. * exception.
  581. *
  582. * ``` php
  583. * <?php
  584. * $user = Stub::make('User', array('getName' => Stub::exactly(3, function() { return 'Davert';}), 'someMethod' => function() {}));
  585. * $user->getName();
  586. * $user->getName();
  587. * $user->getName();
  588. * ?>
  589. * ```
  590. *
  591. * @param int $count
  592. * @param mixed $params
  593. *
  594. * @return StubMarshaler
  595. */
  596. public static function exactly($count, $params = null)
  597. {
  598. return new StubMarshaler(
  599. new \PHPUnit_Framework_MockObject_Matcher_InvokedCount($count),
  600. self::closureIfNull($params)
  601. );
  602. }
  603. private static function closureIfNull($params)
  604. {
  605. if ($params == null) {
  606. return function () {
  607. };
  608. } else {
  609. return $params;
  610. }
  611. }
  612. }
  613. /**
  614. * Holds matcher and value of mocked method
  615. */
  616. class StubMarshaler
  617. {
  618. private $methodMatcher;
  619. private $methodValue;
  620. public function __construct(\PHPUnit_Framework_MockObject_Matcher_InvokedRecorder $matcher, $value)
  621. {
  622. $this->methodMatcher = $matcher;
  623. $this->methodValue = $value;
  624. }
  625. public function getMatcher()
  626. {
  627. return $this->methodMatcher;
  628. }
  629. public function getValue()
  630. {
  631. return $this->methodValue;
  632. }
  633. }