Stub.php 21 KB

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