1: <?php
2:
3: /**
4: * Copyright 2015, Eduardo Trujillo
5: *
6: * For the full copyright and license information, please view the LICENSE
7: * file that was distributed with this source code.
8: *
9: * This file is part of the Nucleus package
10: */
11:
12: namespace Chromabits\Nucleus\Testing;
13:
14: use Chromabits\Nucleus\Exceptions\LackOfCoffeeException;
15: use Chromabits\Nucleus\Exceptions\ResolutionException;
16: use Chromabits\Nucleus\Foundation\BaseObject;
17: use Chromabits\Nucleus\Meditation\Arguments;
18: use Chromabits\Nucleus\Meditation\Boa;
19: use Chromabits\Nucleus\Support\Arr;
20: use Chromabits\Nucleus\Support\Std;
21: use Chromabits\Nucleus\Testing\Mocking\CallAndThrowExpectation;
22: use Chromabits\Nucleus\Testing\Mocking\CallExpectation;
23: use Closure;
24: use Mockery;
25: use Mockery\MockInterface;
26: use ReflectionClass;
27: use ReflectionParameter;
28:
29: /**
30: * Class Impersonator.
31: *
32: * Automatically builds and injects mocks for testing.
33: *
34: * @author Eduardo Trujillo <ed@chromabits.com>
35: * @package Chromabits\Nucleus\Testing
36: */
37: class Impersonator extends BaseObject
38: {
39: /**
40: * List of provided mocks.
41: *
42: * @var array
43: */
44: protected $provided;
45:
46: /**
47: * Construct an instance of an Impersonator.
48: */
49: public function __construct()
50: {
51: parent::__construct();
52:
53: $this->provided = [];
54: }
55:
56: /**
57: * @return Impersonator
58: */
59: public static function define()
60: {
61: return new self();
62: }
63:
64: /**
65: * Attempt to build the provided class.
66: *
67: * Be aware that complex classes might not be resolved automatically.
68: * For example, scalar types are currently not supported.
69: *
70: * @param string $target
71: *
72: * @throws ResolutionException
73: * @return mixed
74: */
75: public function make($target)
76: {
77: $arguments = $this->getArgumentTypes($target);
78:
79: $resolved = $this->mockArguments($arguments);
80:
81: return new $target(...$resolved);
82: }
83:
84: /**
85: * Provide a mock.
86: *
87: * Here we do some "magic" to attempt to figure out what the mock
88: * implements. In order for mock resolution to be fast, relationships
89: * between types and mocks are stored on a hash table ($this->provided).
90: * This means that if you have objects implementing the same interface or
91: * that are instances of the same class, then the last object provided
92: * will be the one used.
93: *
94: * For scenarios where you have two parameters of the same type in the
95: * constructor or conflicting interfaces, it is recommended that you build
96: * the object manually.
97: *
98: * @param mixed $mock
99: *
100: * @throws LackOfCoffeeException
101: * @return $this
102: */
103: public function provide($mock)
104: {
105: if (is_string($mock) || is_array($mock)) {
106: throw new LackOfCoffeeException(
107: 'A mock cannot be a string or an array.'
108: );
109: }
110:
111: $interfaces = class_implements($mock);
112: $parents = class_parents($mock);
113:
114: foreach ($interfaces as $interface) {
115: $this->provided[$interface] = $mock;
116: }
117:
118: foreach ($parents as $parent) {
119: $this->provided[$parent] = $mock;
120: }
121:
122: $this->provided[get_class($mock)] = $mock;
123:
124: return $this;
125: }
126:
127: /**
128: * A shortcut for building mocks.
129: *
130: * @param string $type
131: * @param Closure|CallExpectation[] $definition
132: *
133: * @return $this
134: */
135: public function mock($type, $definition)
136: {
137: Arguments::define(
138: Boa::string(),
139: Boa::either(Boa::func(), Boa::arrOf(
140: Boa::instance(CallExpectation::class)
141: ))
142: )->check($type, $definition);
143:
144: if (is_array($definition)) {
145: $this->provide(static::expectationsToMock(
146: $type,
147: $definition
148: ));
149:
150: return $this;
151: }
152:
153: $this->provide(Mockery::mock($type, $definition));
154:
155: return $this;
156: }
157:
158: /**
159: * Build a mock from an array of CallExpectations.
160: *
161: * @param string $type
162: * @param CallExpectation[] $expectations
163: *
164: * @return MockInterface
165: */
166: public static function expectationsToMock($type, $expectations)
167: {
168: return Mockery::mock(
169: $type,
170: function (MockInterface $mock) use ($expectations) {
171: Std::each(
172: function (CallExpectation $expect) use (&$mock) {
173: $mockExpect = $mock
174: ->shouldReceive($expect->getMethodName())
175: ->times($expect->getTimes())
176: ->withArgs($expect->getArguments())
177: ->andReturn($expect->getReturn());
178:
179: if ($expect instanceof CallAndThrowExpectation) {
180: $mockExpect->andThrow(
181: $expect->getExceptionClass(),
182: $expect->getExceptionMessage(),
183: $expect->getExceptionCode()
184: );
185: }
186: },
187: $expectations
188: );
189: }
190: );
191: }
192:
193: /**
194: * Reflect about a class' constructor parameter types.
195: *
196: * @param mixed $target
197: *
198: * @throws LackOfCoffeeException
199: * @return ReflectionParameter[]
200: */
201: protected function getArgumentTypes($target)
202: {
203: $reflect = new ReflectionClass($target);
204:
205: if ($reflect->getConstructor() === null) {
206: return [];
207: }
208:
209: return $reflect->getConstructor()->getParameters();
210: }
211:
212: /**
213: * Attempt to automatically mock the arguments of a function.
214: *
215: * @param ReflectionParameter[] $parameters
216: * @param array $overrides
217: *
218: * @return array
219: * @throws ResolutionException
220: */
221: protected function mockArguments(array $parameters, $overrides = [])
222: {
223: $resolved = [];
224:
225: foreach ($parameters as $parameter) {
226: $hint = $parameter->getClass();
227: $name = $parameter->getName();
228:
229: if (Arr::has($overrides, $name)) {
230: $resolved[] = $overrides[$name];
231:
232: continue;
233: }
234:
235: if (is_null($hint)) {
236: throw new ResolutionException();
237: }
238:
239: $mock = $this->resolveMock($hint);
240:
241: $resolved[] = $mock;
242: }
243:
244: return $resolved;
245: }
246:
247: /**
248: * Resolve which mock instance to use.
249: *
250: * Here we mainly decide whether to use something that was provided to or
251: * go ahead an build an empty mock.
252: *
253: * @param ReflectionClass $type
254: *
255: * @return MockInterface
256: */
257: protected function resolveMock(ReflectionClass $type)
258: {
259: $name = $type->getName();
260:
261: if (array_key_exists($name, $this->provided)) {
262: return $this->provided[$name];
263: }
264:
265: return $this->buildMock($type);
266: }
267:
268: /**
269: * Build an empty mock.
270: *
271: * Override this method if you would like to use a different mocking library
272: * or if you would like all your mocks having some properties in common.
273: *
274: * @param ReflectionClass $type
275: *
276: * @return MockInterface
277: */
278: protected function buildMock(ReflectionClass $type)
279: {
280: return Mockery::mock($type->getName());
281: }
282:
283: /**
284: * Shortcut for constructing an instance of a CallExpectation.
285: *
286: * @param string $methodName
287: * @param array $arguments
288: * @param mixed|null $return
289: * @param int $times
290: *
291: * @return CallExpectation
292: */
293: public static function on(
294: $methodName,
295: array $arguments,
296: $return = null,
297: $times = 1
298: ) {
299: return new CallExpectation($methodName, $arguments, $return, $times);
300: }
301:
302: /**
303: * Shortcut for constructing an instance of a CallAndThrowExpectation.
304: *
305: * @param string $methodName
306: * @param array $arguments
307: * @param string $exceptionClass
308: * @param string $exceptionMessage
309: * @param int $exceptionCode
310: *
311: * @return CallAndThrowExpectation
312: */
313: public static function throwOn(
314: $methodName,
315: array $arguments,
316: $exceptionClass,
317: $exceptionMessage,
318: $exceptionCode
319: ) {
320: return new CallAndThrowExpectation(
321: $methodName,
322: $arguments,
323: $exceptionClass,
324: $exceptionMessage,
325: $exceptionCode
326: );
327: }
328:
329: /**
330: * Construct an instance of the target class and call the method while
331: * injecting any argument that was not provided.
332: *
333: * @param string $target
334: * @param string $methodName
335: * @param array $arguments
336: *
337: * @return mixed
338: */
339: public function makeAndCall($target, $methodName, array $arguments = [])
340: {
341: return $this->call($this->make($target), $methodName, $arguments);
342: }
343:
344: /**
345: * Call the method on the target object while injecting any missing
346: * arguments using objects defined on this Impersonator instance.
347: *
348: * This allows one to easily call methods that define dependencies in their
349: * arguments rather than just on the constructor of the class they reside
350: * in.
351: *
352: * Impersonator will apply a similar algorithm to make(). Dependencies that
353: * are not provided, will be automatically be replaced with a dummy mock.
354: * However, in the case of method calls, any provided argument will take
355: * precedence over any injection.
356: *
357: * @param mixed $target
358: * @param string $methodName
359: * @param array $arguments
360: *
361: * @return mixed
362: */
363: public function call($target, $methodName, array $arguments = [])
364: {
365: $reflection = new ReflectionClass($target);
366:
367: $resolved = $this->mockArguments(
368: $reflection->getMethod($methodName)->getParameters(),
369: $arguments
370: );
371:
372: return $target->$methodName(...$resolved);
373: }
374: }
375: