You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

404 lines
15 KiB

3 years ago
  1. # Prophecy
  2. [![Stable release](https://poser.pugx.org/phpspec/prophecy/version.svg)](https://packagist.org/packages/phpspec/prophecy)
  3. [![Build Status](https://travis-ci.org/phpspec/prophecy.svg?branch=master)](https://travis-ci.org/phpspec/prophecy)
  4. Prophecy is a highly opinionated yet very powerful and flexible PHP object mocking
  5. framework. Though initially it was created to fulfil phpspec2 needs, it is flexible
  6. enough to be used inside any testing framework out there with minimal effort.
  7. ## A simple example
  8. ```php
  9. <?php
  10. class UserTest extends PHPUnit\Framework\TestCase
  11. {
  12. private $prophet;
  13. public function testPasswordHashing()
  14. {
  15. $hasher = $this->prophet->prophesize('App\Security\Hasher');
  16. $user = new App\Entity\User($hasher->reveal());
  17. $hasher->generateHash($user, 'qwerty')->willReturn('hashed_pass');
  18. $user->setPassword('qwerty');
  19. $this->assertEquals('hashed_pass', $user->getPassword());
  20. }
  21. protected function setUp()
  22. {
  23. $this->prophet = new \Prophecy\Prophet;
  24. }
  25. protected function tearDown()
  26. {
  27. $this->prophet->checkPredictions();
  28. }
  29. }
  30. ```
  31. ## Installation
  32. ### Prerequisites
  33. Prophecy requires PHP 7.2.0 or greater.
  34. ### Setup through composer
  35. First, add Prophecy to the list of dependencies inside your `composer.json`:
  36. ```json
  37. {
  38. "require-dev": {
  39. "phpspec/prophecy": "~1.0"
  40. }
  41. }
  42. ```
  43. Then simply install it with composer:
  44. ```bash
  45. $> composer install --prefer-dist
  46. ```
  47. You can read more about Composer on its [official webpage](http://getcomposer.org).
  48. ## How to use it
  49. First of all, in Prophecy every word has a logical meaning, even the name of the library
  50. itself (Prophecy). When you start feeling that, you'll become very fluid with this
  51. tool.
  52. For example, Prophecy has been named that way because it concentrates on describing the future
  53. behavior of objects with very limited knowledge about them. But as with any other prophecy,
  54. those object prophecies can't create themselves - there should be a Prophet:
  55. ```php
  56. $prophet = new Prophecy\Prophet;
  57. ```
  58. The Prophet creates prophecies by *prophesizing* them:
  59. ```php
  60. $prophecy = $prophet->prophesize();
  61. ```
  62. The result of the `prophesize()` method call is a new object of class `ObjectProphecy`. Yes,
  63. that's your specific object prophecy, which describes how your object would behave
  64. in the near future. But first, you need to specify which object you're talking about,
  65. right?
  66. ```php
  67. $prophecy->willExtend('stdClass');
  68. $prophecy->willImplement('SessionHandlerInterface');
  69. ```
  70. There are 2 interesting calls - `willExtend` and `willImplement`. The first one tells
  71. object prophecy that our object should extend a specific class. The second one says that
  72. it should implement some interface. Obviously, objects in PHP can implement multiple
  73. interfaces, but extend only one parent class.
  74. ### Dummies
  75. Ok, now we have our object prophecy. What can we do with it? First of all, we can get
  76. our object *dummy* by revealing its prophecy:
  77. ```php
  78. $dummy = $prophecy->reveal();
  79. ```
  80. The `$dummy` variable now holds a special dummy object. Dummy objects are objects that extend
  81. and/or implement preset classes/interfaces by overriding all their public methods. The key
  82. point about dummies is that they do not hold any logic - they just do nothing. Any method
  83. of the dummy will always return `null` and the dummy will never throw any exceptions.
  84. Dummy is your friend if you don't care about the actual behavior of this double and just need
  85. a token object to satisfy a method typehint.
  86. You need to understand one thing - a dummy is not a prophecy. Your object prophecy is still
  87. assigned to `$prophecy` variable and in order to manipulate with your expectations, you
  88. should work with it. `$dummy` is a dummy - a simple php object that tries to fulfil your
  89. prophecy.
  90. ### Stubs
  91. Ok, now we know how to create basic prophecies and reveal dummies from them. That's
  92. awesome if we don't care about our _doubles_ (objects that reflect originals)
  93. interactions. If we do, we need to use *stubs* or *mocks*.
  94. A stub is an object double, which doesn't have any expectations about the object behavior,
  95. but when put in specific environment, behaves in specific way. Ok, I know, it's cryptic,
  96. but bear with me for a minute. Simply put, a stub is a dummy, which depending on the called
  97. method signature does different things (has logic). To create stubs in Prophecy:
  98. ```php
  99. $prophecy->read('123')->willReturn('value');
  100. ```
  101. Oh wow. We've just made an arbitrary call on the object prophecy? Yes, we did. And this
  102. call returned us a new object instance of class `MethodProphecy`. Yep, that's a specific
  103. method with arguments prophecy. Method prophecies give you the ability to create method
  104. promises or predictions. We'll talk about method predictions later in the _Mocks_ section.
  105. #### Promises
  106. Promises are logical blocks, that represent your fictional methods in prophecy terms
  107. and they are handled by the `MethodProphecy::will(PromiseInterface $promise)` method.
  108. As a matter of fact, the call that we made earlier (`willReturn('value')`) is a simple
  109. shortcut to:
  110. ```php
  111. $prophecy->read('123')->will(new Prophecy\Promise\ReturnPromise(array('value')));
  112. ```
  113. This promise will cause any call to our double's `read()` method with exactly one
  114. argument - `'123'` to always return `'value'`. But that's only for this
  115. promise, there's plenty others you can use:
  116. - `ReturnPromise` or `->willReturn(1)` - returns a value from a method call
  117. - `ReturnArgumentPromise` or `->willReturnArgument($index)` - returns the nth method argument from call
  118. - `ThrowPromise` or `->willThrow($exception)` - causes the method to throw specific exception
  119. - `CallbackPromise` or `->will($callback)` - gives you a quick way to define your own custom logic
  120. Keep in mind, that you can always add even more promises by implementing
  121. `Prophecy\Promise\PromiseInterface`.
  122. #### Method prophecies idempotency
  123. Prophecy enforces same method prophecies and, as a consequence, same promises and
  124. predictions for the same method calls with the same arguments. This means:
  125. ```php
  126. $methodProphecy1 = $prophecy->read('123');
  127. $methodProphecy2 = $prophecy->read('123');
  128. $methodProphecy3 = $prophecy->read('321');
  129. $methodProphecy1 === $methodProphecy2;
  130. $methodProphecy1 !== $methodProphecy3;
  131. ```
  132. That's interesting, right? Now you might ask me how would you define more complex
  133. behaviors where some method call changes behavior of others. In PHPUnit or Mockery
  134. you do that by predicting how many times your method will be called. In Prophecy,
  135. you'll use promises for that:
  136. ```php
  137. $user->getName()->willReturn(null);
  138. // For PHP 5.4
  139. $user->setName('everzet')->will(function () {
  140. $this->getName()->willReturn('everzet');
  141. });
  142. // For PHP 5.3
  143. $user->setName('everzet')->will(function ($args, $user) {
  144. $user->getName()->willReturn('everzet');
  145. });
  146. // Or
  147. $user->setName('everzet')->will(function ($args) use ($user) {
  148. $user->getName()->willReturn('everzet');
  149. });
  150. ```
  151. And now it doesn't matter how many times or in which order your methods are called.
  152. What matters is their behaviors and how well you faked it.
  153. Note: If the method is called several times, you can use the following syntax to return different
  154. values for each call:
  155. ```php
  156. $prophecy->read('123')->willReturn(1, 2, 3);
  157. ```
  158. This feature is actually not recommended for most cases. Relying on the order of
  159. calls for the same arguments tends to make test fragile, as adding one more call
  160. can break everything.
  161. #### Arguments wildcarding
  162. The previous example is awesome (at least I hope it is for you), but that's not
  163. optimal enough. We hardcoded `'everzet'` in our expectation. Isn't there a better
  164. way? In fact there is, but it involves understanding what this `'everzet'`
  165. actually is.
  166. You see, even if method arguments used during method prophecy creation look
  167. like simple method arguments, in reality they are not. They are argument token
  168. wildcards. As a matter of fact, `->setName('everzet')` looks like a simple call just
  169. because Prophecy automatically transforms it under the hood into:
  170. ```php
  171. $user->setName(new Prophecy\Argument\Token\ExactValueToken('everzet'));
  172. ```
  173. Those argument tokens are simple PHP classes, that implement
  174. `Prophecy\Argument\Token\TokenInterface` and tell Prophecy how to compare real arguments
  175. with your expectations. And yes, those classnames are damn big. That's why there's a
  176. shortcut class `Prophecy\Argument`, which you can use to create tokens like that:
  177. ```php
  178. use Prophecy\Argument;
  179. $user->setName(Argument::exact('everzet'));
  180. ```
  181. `ExactValueToken` is not very useful in our case as it forced us to hardcode the username.
  182. That's why Prophecy comes bundled with a bunch of other tokens:
  183. - `IdenticalValueToken` or `Argument::is($value)` - checks that the argument is identical to a specific value
  184. - `ExactValueToken` or `Argument::exact($value)` - checks that the argument matches a specific value
  185. - `TypeToken` or `Argument::type($typeOrClass)` - checks that the argument matches a specific type or
  186. classname
  187. - `ObjectStateToken` or `Argument::which($method, $value)` - checks that the argument method returns
  188. a specific value
  189. - `CallbackToken` or `Argument::that(callback)` - checks that the argument matches a custom callback
  190. - `AnyValueToken` or `Argument::any()` - matches any argument
  191. - `AnyValuesToken` or `Argument::cetera()` - matches any arguments to the rest of the signature
  192. - `StringContainsToken` or `Argument::containingString($value)` - checks that the argument contains a specific string value
  193. - `InArrayToken` or `Argument::in($array)` - checks if value is in array
  194. - `NotInArrayToken` or `Argument::notIn($array)` - checks if value is not in array
  195. And you can add even more by implementing `TokenInterface` with your own custom classes.
  196. So, let's refactor our initial `{set,get}Name()` logic with argument tokens:
  197. ```php
  198. use Prophecy\Argument;
  199. $user->getName()->willReturn(null);
  200. // For PHP 5.4
  201. $user->setName(Argument::type('string'))->will(function ($args) {
  202. $this->getName()->willReturn($args[0]);
  203. });
  204. // For PHP 5.3
  205. $user->setName(Argument::type('string'))->will(function ($args, $user) {
  206. $user->getName()->willReturn($args[0]);
  207. });
  208. // Or
  209. $user->setName(Argument::type('string'))->will(function ($args) use ($user) {
  210. $user->getName()->willReturn($args[0]);
  211. });
  212. ```
  213. That's it. Now our `{set,get}Name()` prophecy will work with any string argument provided to it.
  214. We've just described how our stub object should behave, even though the original object could have
  215. no behavior whatsoever.
  216. One last bit about arguments now. You might ask, what happens in case of:
  217. ```php
  218. use Prophecy\Argument;
  219. $user->getName()->willReturn(null);
  220. // For PHP 5.4
  221. $user->setName(Argument::type('string'))->will(function ($args) {
  222. $this->getName()->willReturn($args[0]);
  223. });
  224. // For PHP 5.3
  225. $user->setName(Argument::type('string'))->will(function ($args, $user) {
  226. $user->getName()->willReturn($args[0]);
  227. });
  228. // Or
  229. $user->setName(Argument::type('string'))->will(function ($args) use ($user) {
  230. $user->getName()->willReturn($args[0]);
  231. });
  232. $user->setName(Argument::any())->will(function () {
  233. });
  234. ```
  235. Nothing. Your stub will continue behaving the way it did before. That's because of how
  236. arguments wildcarding works. Every argument token type has a different score level, which
  237. wildcard then uses to calculate the final arguments match score and use the method prophecy
  238. promise that has the highest score. In this case, `Argument::type()` in case of success
  239. scores `5` and `Argument::any()` scores `3`. So the type token wins, as does the first
  240. `setName()` method prophecy and its promise. The simple rule of thumb - more precise token
  241. always wins.
  242. #### Getting stub objects
  243. Ok, now we know how to define our prophecy method promises, let's get our stub from
  244. it:
  245. ```php
  246. $stub = $prophecy->reveal();
  247. ```
  248. As you might see, the only difference between how we get dummies and stubs is that with
  249. stubs we describe every object conversation instead of just agreeing with `null` returns
  250. (object being *dummy*). As a matter of fact, after you define your first promise
  251. (method call), Prophecy will force you to define all the communications - it throws
  252. the `UnexpectedCallException` for any call you didn't describe with object prophecy before
  253. calling it on a stub.
  254. ### Mocks
  255. Now we know how to define doubles without behavior (dummies) and doubles with behavior, but
  256. no expectations (stubs). What's left is doubles for which we have some expectations. These
  257. are called mocks and in Prophecy they look almost exactly the same as stubs, except that
  258. they define *predictions* instead of *promises* on method prophecies:
  259. ```php
  260. $entityManager->flush()->shouldBeCalled();
  261. ```
  262. #### Predictions
  263. The `shouldBeCalled()` method here assigns `CallPrediction` to our method prophecy.
  264. Predictions are a delayed behavior check for your prophecies. You see, during the entire lifetime
  265. of your doubles, Prophecy records every single call you're making against it inside your
  266. code. After that, Prophecy can use this collected information to check if it matches defined
  267. predictions. You can assign predictions to method prophecies using the
  268. `MethodProphecy::should(PredictionInterface $prediction)` method. As a matter of fact,
  269. the `shouldBeCalled()` method we used earlier is just a shortcut to:
  270. ```php
  271. $entityManager->flush()->should(new Prophecy\Prediction\CallPrediction());
  272. ```
  273. It checks if your method of interest (that matches both the method name and the arguments wildcard)
  274. was called 1 or more times. If the prediction failed then it throws an exception. When does this
  275. check happen? Whenever you call `checkPredictions()` on the main Prophet object:
  276. ```php
  277. $prophet->checkPredictions();
  278. ```
  279. In PHPUnit, you would want to put this call into the `tearDown()` method. If no predictions
  280. are defined, it would do nothing. So it won't harm to call it after every test.
  281. There are plenty more predictions you can play with:
  282. - `CallPrediction` or `shouldBeCalled()` - checks that the method has been called 1 or more times
  283. - `NoCallsPrediction` or `shouldNotBeCalled()` - checks that the method has not been called
  284. - `CallTimesPrediction` or `shouldBeCalledTimes($count)` - checks that the method has been called
  285. `$count` times
  286. - `CallbackPrediction` or `should($callback)` - checks the method against your own custom callback
  287. Of course, you can always create your own custom prediction any time by implementing
  288. `PredictionInterface`.
  289. ### Spies
  290. The last bit of awesomeness in Prophecy is out-of-the-box spies support. As I said in the previous
  291. section, Prophecy records every call made during the double's entire lifetime. This means
  292. you don't need to record predictions in order to check them. You can also do it
  293. manually by using the `MethodProphecy::shouldHave(PredictionInterface $prediction)` method:
  294. ```php
  295. $em = $prophet->prophesize('Doctrine\ORM\EntityManager');
  296. $controller->createUser($em->reveal());
  297. $em->flush()->shouldHaveBeenCalled();
  298. ```
  299. Such manipulation with doubles is called spying. And with Prophecy it just works.