Skip to content

Commit fd812b7

Browse files
committed
Enhance ContainerDoubleTrait
It is now possible * to specify a target class to prophesize. * to specify interfaces the prophecy should implement * to specify constructor arguments for the prophecy * to pass additional arguments to predict get calls globally or per service * to pass additional arguments to predict has calls globally or per service * to specify the promise method (will, willReturn, etc) for the get call globally or per service
1 parent b09a973 commit fd812b7

2 files changed

Lines changed: 238 additions & 40 deletions

File tree

src/TestCase/ContainerDoubleTrait.php

Lines changed: 79 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
use Prophecy\Prophecy\ObjectProphecy;
1616

1717
/**
18-
* Creates a service manager prophecy or double with configured services.
18+
* Creates a service container prophecy or double with configured services.
1919
*
2020
* @method \Prophecy\Prophecy\ObjectProphecy prophesize(string $classOrInterface)
2121
*
@@ -27,67 +27,118 @@ trait ContainerDoubleTrait
2727
* Prohesizes a container interface.
2828
*
2929
* Pass in services as iterable:
30-
* <pre>
30+
* ```
3131
* [
32+
* // short
3233
* 'name' => service,
34+
*
35+
* // verbose style
3336
* 'name' => [
3437
* 'service' => service,
38+
* 'args_get' => array: Additional arguments the get
39+
* method is called with
40+
* 'args_has' => array: Additional arguments the has
41+
* method is called with
3542
* 'count_get' => int,
3643
* 'count_has' => int,
44+
* 'promise' => string: The promise method for the
45+
* prophecy
46+
* * willReturn (default)
47+
* * willThrow
48+
* * will
49+
* * _etc._
3750
* ],
51+
*
52+
* // compact style
3853
* 'name' => [service{, count_get{, count_has}}],
54+
*
55+
* // mixed (example)
56+
* 'name' => [service, count_get, 'args_get' => ['arg'], count_has]
3957
* ]
40-
* </pre>
58+
* ```
4159
*
42-
* Passing the boolean value 'false' as service will cause the following:
43-
* - Calling has(service) will return false.
44-
* - Calling get(service) will throw an exception.
60+
* Passing the boolean value _false_ as service will cause the following:
61+
* * Calling has(service) will return false.
62+
* * Calling get(service) will throw an exception.
4563
*
64+
* ### Options
65+
* ```
66+
* [
67+
* 'target' => FQCN of the Container class
68+
* default: \Psr\Container\ContainerInterface
69+
*
70+
* 'arguments' => array of constructor arguments for the container
71+
* 'implements' => array of interfaces the container
72+
* should implement
73+
* 'args_get' => default arguments for the get method
74+
* for each service
75+
* 'args_has' => default arguments for the has method
76+
* for each service
77+
* 'promise' => default promise method for each service
78+
* ]
79+
* ```
4680
* @param iterable $services
81+
* @param array $options
4782
*
4883
* @return ObjectProphecy
4984
*/
50-
public function createContainerProphecy(iterable $services = []): ObjectProphecy
85+
public function createContainerProphecy(iterable $services = [], array $options = []): ObjectProphecy
5186
{
52-
if (!interface_exists(\Psr\Container\ContainerInterface::class)) {
87+
$target = $options['target'] ?? \Psr\Container\ContainerInterface::class;
88+
89+
if (!interface_exists($target) && !class_exists($target)) {
5390
throw InvalidUsageException::fromTrait(
5491
__TRAIT__,
55-
get_class($this),
56-
'Cannot create container double. Interface %s does not exist.',
57-
\Psr\Container\ContainerInterface::class
92+
__CLASS__,
93+
'Cannot create container double. Interface or class %s does not exist.',
94+
$target
5895
);
5996
}
6097

61-
$container = $this->prophesize(\Psr\Container\ContainerInterface::class);
98+
$container = $this->prophesize($target);
99+
100+
if (isset($options['arguments'])) {
101+
$container->willBeConstructedWith($options['arguments']);
102+
}
103+
104+
if (isset($options['implements'])) {
105+
foreach ((array) $options['implements'] as $interface) {
106+
$container->willImplement($interface);
107+
}
108+
}
62109

63110
foreach ($services as $name => $spec) {
64111
if (!is_array($spec)) {
65112
$spec = ['service' => $spec];
66113
}
67114

68-
$countGet = $spec['count_get'] ?? $spec[1] ?? 0;
69-
$countHas = $spec['count_has'] ?? $spec[2] ?? 0;
70-
$service = $spec['service'] ?? $spec[0] ?? null;
115+
$service = $spec['service'] ?? $spec[0] ?? null;
116+
$count = $spec['count_get'] ?? $spec[1] ?? $options['count_get'] ?? 0;
117+
$args = $spec['args_get'] ?? $options['args_get'] ?? [];
71118

72119
/** @var \Prophecy\Prophecy\MethodProphecy $method */
73-
$method = $container->get($name);
120+
$method = $container->get($name, ...$args);
74121

75122
if (false === $service) {
76123
$ex = $this->prophesize(\Psr\Container\NotFoundExceptionInterface::class)->reveal();
77124
$method->willThrow($ex);
78125
} else {
79-
$method->willReturn($service);
126+
$promise = $spec['promise'] ?? $options['promise'] ?? 'willReturn';
127+
$method->$promise($service);
80128
}
81129

82-
if ($countGet) {
83-
$method->shouldBeCalledTimes($countGet);
130+
if ($count) {
131+
$method->shouldBeCalledTimes($count);
84132
}
85133

86-
$method = $container->has($name);
134+
$args = $spec['args_has'] ?? $options['args_has'] ?? [];
135+
$count = $spec['count_has'] ?? $spec[2] ?? $options['count_has'] ?? 0;
136+
$method = $container->has($name, ...$args);
137+
87138
$method->willReturn(false !== $service);
88139

89-
if ($countHas) {
90-
$method->shouldBeCalledTimes($countHas);
140+
if ($count) {
141+
$method->shouldBeCalledTimes($count);
91142
}
92143
}
93144

@@ -96,14 +147,16 @@ public function createContainerProphecy(iterable $services = []): ObjectProphecy
96147

97148
/**
98149
* Creates a container interface double.
150+
* see {@link createContainerProphecy()}
99151
*
100-
* @param iterable $services The services the container should provide
101-
* see {@link createContainerProphecy()}
152+
* @param iterable $services
153+
* @param array $options
102154
*
155+
* @see createContainerProphecy()
103156
* @return object The revealed container double
104157
*/
105-
public function createContainerDouble(iterable $services = []): object
158+
public function createContainerDouble(iterable $services = [], array $options = []): object
106159
{
107-
return $this->createContainerProphecy($services)->reveal();
160+
return $this->createContainerProphecy($services, $options)->reveal();
108161
}
109162
}

test/TestUtilsTest/TestCase/ContainerDoubleTraitTest.php

Lines changed: 159 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,8 @@
1212
namespace Cross\TestUtilsTest\TestCase;
1313

1414
use Cross\TestUtils\Exception\InvalidUsageException;
15-
1615
use Cross\TestUtils\TestCase\ContainerDoubleTrait;
17-
1816
use Prophecy\Argument;
19-
2017
use phpmock\prophecy\PHPProphet;
2118

2219
/**
@@ -112,16 +109,9 @@ public function servicesProvider()
112109
];
113110
}
114111

115-
/**
116-
* @dataProvider servicesProvider
117-
*
118-
* @param array $services
119-
* @param array $expect
120-
*/
121-
public function testCreatesContainerDouble(array $services, array $expect)
112+
private function getConcreteTraitClass()
122113
{
123-
$scope = $this;
124-
$target = new class
114+
return new class
125115
{
126116
use ContainerDoubleTrait;
127117

@@ -155,21 +145,177 @@ public function reveal()
155145

156146
return $this;
157147
}
148+
149+
public function willBeConstructedWith(array $args = null)
150+
{
151+
$this->__call(__FUNCTION__, [$args]);
152+
}
153+
154+
public function willImplement($interface)
155+
{
156+
$this->__call(__FUNCTION__, [$interface]);
157+
}
158158
};
159159
}
160160
};
161161

162+
}
163+
164+
/**
165+
* @dataProvider servicesProvider
166+
*
167+
* @param array $services
168+
* @param array $expect
169+
*/
170+
public function testCreatesContainerDouble(array $services, array $expect)
171+
{
172+
$target = $this->getConcreteTraitClass();
162173
$actual = $target->createContainerDouble($services);
163174

164175
static::assertEquals(\Psr\Container\ContainerInterface::class, $actual->name);
165176
static::assertEquals($expect, $actual->calls);
166177
}
167178

168-
public function testThrowsExceptionIfInterfaceIsNotDefined()
179+
public function servicesOptionsProvider()
180+
{
181+
$target = new class {};
182+
$targetFqcn = get_class($target);
183+
return [
184+
[
185+
[],
186+
['target' => $targetFqcn],
187+
[],
188+
],
189+
[
190+
[],
191+
['implements' => 'SomeInterface'],
192+
[['willImplement', ['SomeInterface']]]
193+
],
194+
[
195+
[],
196+
['implements' => ['SomeInterface', 'Other']],
197+
[['willImplement', ['SomeInterface'], ['willIpmlement', ['Other']]]],
198+
],
199+
[
200+
[],
201+
['arguments' => ['arg1', 'arg2']],
202+
[['willBeConstructedWith', [['arg1', 'arg2']]]],
203+
],
204+
[
205+
[
206+
'name' => [
207+
'service',
208+
'args_get' => ['arg1', 'arg2'],
209+
'args_has' => ['has1', 'has2'],
210+
]
211+
],
212+
[],
213+
[
214+
['get', ['name', 'arg1', 'arg2']],
215+
['has', ['name', 'has1', 'has2']],
216+
],
217+
],
218+
// global options
219+
[
220+
[
221+
'name' => [
222+
'service',
223+
]
224+
],
225+
[
226+
'args_get' => ['global1', 'global2'],
227+
'args_has' => ['hasglobal1', 'hasglobal2'],
228+
],
229+
[
230+
['get', ['name', 'global1', 'global2']],
231+
['has', ['name', 'hasglobal1', 'hasglobal2']],
232+
],
233+
],
234+
// global options override
235+
[
236+
[
237+
'name' => [
238+
'service',
239+
'args_get' => ['arg1', 'arg2'],
240+
'args_has' => ['has1', 'has2'],
241+
]
242+
],
243+
[
244+
'args_get' => ['global1', 'global2']
245+
],
246+
[
247+
['get', ['name', 'arg1', 'arg2']],
248+
['has', ['name', 'has1', 'has2']],
249+
],
250+
],
251+
// promises
252+
[
253+
[
254+
'name' => [
255+
'service',
256+
'promise' => 'will'
257+
]
258+
],
259+
[],
260+
[
261+
['will', ['service']]
262+
]
263+
],
264+
// global promise
265+
[
266+
[
267+
'name' => [
268+
'service',
269+
]
270+
],
271+
['promise' => 'will'],
272+
[
273+
['will', ['service']]
274+
]
275+
],
276+
[
277+
[
278+
'name' => [
279+
'service',
280+
'promise' => 'will'
281+
]
282+
],
283+
['promise' => 'willReturn'],
284+
[
285+
['will', ['service']]
286+
]
287+
],
288+
289+
];
290+
}
291+
292+
/**
293+
* @dataProvider servicesOptionsProvider
294+
* @param array $services
295+
* @param array $options
296+
* @param array $expect
297+
* @return void
298+
*/
299+
public function testCreatesContainerProphecyWithOptions(array $services, array $options, array $expect)
300+
{
301+
$target = $this->getConcreteTraitClass();
302+
$actual = $target->createContainerProphecy($services, $options);
303+
304+
static::assertEquals($options['target'] ?? \Psr\Container\ContainerInterface::class, $actual->name);
305+
foreach ($expect as $spec) {
306+
$method = $spec[0];
307+
static::assertArrayHasKey($method, $actual->calls);
308+
$actualArgs = $actual->calls[$method];
309+
static::assertContains($spec[1], $actualArgs);
310+
}
311+
}
312+
313+
public function testThrowsExceptionIfInterfaceOrClassIsNotDefined()
169314
{
170315

171316
$func = (new PHPProphet)->prophesize('Cross\TestUtils\TestCase');
172317
$func->interface_exists(Argument::any())->willReturn(false);
318+
$func->class_exists(Argument::any())->willReturn(false);
173319
$func->reveal();
174320

175321
$this->expectException(InvalidUsageException::class);
@@ -179,6 +325,5 @@ public function testThrowsExceptionIfInterfaceIsNotDefined()
179325
};
180326

181327
$target->createContainerProphecy();
182-
183328
}
184329
}

0 commit comments

Comments
 (0)