Skip to content

Commit 2f289f7

Browse files
committed
Add CreateProphecyTrait
This outsources the prophecy creation from the CreateTargetTrait, since Prophecy does not allow partial mocking and thus, creating a prophecy as SUT is pointless.
1 parent d799f7c commit 2f289f7

2 files changed

Lines changed: 350 additions & 0 deletions

File tree

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
<?php
2+
/**
3+
* CROSS PHPunit Utils
4+
*
5+
* @filesource
6+
* @copyright 2019 Cross Solution <https://www.cross-solution.de>
7+
* @license MIT
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace Cross\TestUtils\TestCase;
13+
14+
use Cross\TestUtils\Exception\InvalidUsageException;
15+
use Prophecy\Prophecy\ObjectProphecy;
16+
17+
/**
18+
* Creates object prophecies or doubles.
19+
*
20+
* @author Mathias Gelhausen <gelhausen@cross-solution.de>
21+
* @todo write tests
22+
*/
23+
trait CreateProphecyTrait
24+
{
25+
26+
/**
27+
* Creates an object prohpecy object.
28+
*
29+
* $class can be a string, or an array where the first item is the class name and all
30+
* successive items are constructor arguments.
31+
*
32+
* $class can also be an array using a more verbose style:
33+
* <code>
34+
* [
35+
* 'class' => FQCN, // override FQCN given at key 0
36+
* 'implements' => [InterfaceFQCN, ...],
37+
* 'extends' => FQCN,
38+
* 'arguments' => [argument, ...] // override arguments given without keys
39+
* ]
40+
* </code>
41+
*
42+
* you can mix:
43+
* <code>
44+
* [
45+
* FQCN::class, argument, 'implements' => [Interface::class]
46+
* ]
47+
* </code>
48+
*
49+
* NOTE: If arguments are given without keys, it is assumed that the FQCN is given at the key 0 -
50+
* which is ignored when gathering the arguments. So
51+
* ['class' => FQCN, argument1, argument2] will only take argument2 as argument.
52+
*
53+
* $prophecies is an array of method prophecy specifications:
54+
* [
55+
* [
56+
* methodName, // Call method without arguments.
57+
* methodName => [ ...args ] // Call method with arguments args.
58+
* methodName => arg // Single argument must not be wrapped in array.
59+
* methodName => [[subArg,..]] // Passing an array: Must be wrapped in an array.
60+
* ]
61+
* ]
62+
*
63+
* Please note: method calls in the prophecy specification are chained - the succcessive method
64+
* will be called on the returned value from the method before. The first method is called on the object prophecy.
65+
*
66+
* <example>
67+
* createProphecy(
68+
* [subjectUnderTest::class, argument1, argument2],
69+
* [
70+
* [ 'methodToTest' => ['arg1'], 'willReturn' => true, 'shouldBeCalled' ]
71+
* ]
72+
* );
73+
* </example>
74+
*
75+
* @param string|array $class
76+
* @param array $prophecies
77+
*
78+
* @return ObjectProphecy
79+
*/
80+
public function createProphecy($class, array $prophecies = []): ObjectProphecy
81+
{
82+
[$class, $arguments, $extends, $implements] = $this->createProphecyParseOptions($class);
83+
84+
/** @var ObjectProphecy $prophecy */
85+
$prophecy = $this->prophesize($class);
86+
87+
if ($arguments) {
88+
$prophecy->willBeConstructedWith($arguments);
89+
}
90+
91+
if ($extends) {
92+
$prophecy->willExtend($extends);
93+
}
94+
95+
if ($implements) {
96+
foreach ($implements as $interface) {
97+
$prophecy->willImplement($interface);
98+
}
99+
}
100+
101+
foreach ($prophecies as $methodProphecy) {
102+
$methodMock = $prophecy;
103+
foreach ($methodProphecy as $methodName => $methodArgs) {
104+
if (is_numeric($methodName)) {
105+
$methodMock = $methodMock->$methodArgs();
106+
continue;
107+
}
108+
109+
if (!is_array($methodArgs)) {
110+
$methodArgs = [$methodArgs];
111+
}
112+
113+
$methodMock = $methodMock->$methodName(...$methodArgs);
114+
}
115+
}
116+
117+
return $prophecy;
118+
}
119+
120+
/**
121+
* Creates a double.
122+
*
123+
* @see createProphecy()
124+
*
125+
* @param string|array $class
126+
* @param array $prophecies
127+
*
128+
* @return object
129+
*/
130+
public function createDouble($class, array $prophecies = []): object
131+
{
132+
return $this->createProphecy($class, $prophecies)->reveal();
133+
}
134+
135+
/**
136+
* Normalizes specification.
137+
*
138+
* @param string|array $spec
139+
*
140+
* @return array
141+
*/
142+
private function createProphecyParseOptions($spec): array
143+
{
144+
if (is_string($spec)) {
145+
return [$spec, false, false, false];
146+
}
147+
148+
if (!is_array($spec)) {
149+
throw InvalidUsageException::fromTrait(
150+
__TRAIT__,
151+
__CLASS__,
152+
'Expected string or array, but received %s',
153+
gettype($spec)
154+
);
155+
}
156+
157+
$class = $spec['class'] ?? $spec[0] ?? null;
158+
$arguments = $spec['arguments'] ?? false;
159+
$extends = $spec['extends'] ?? false;
160+
$implements = $spec['implements'] ?? false;
161+
162+
if (!$class) {
163+
throw InvalidUsageException::fromTrait(
164+
__TRAIT__,
165+
__CLASS__,
166+
'No FQCN found.'
167+
);
168+
}
169+
170+
if (!$arguments) {
171+
$spec = array_filter(
172+
$spec,
173+
function ($key) {
174+
return is_numeric($key) && 0 != $key;
175+
},
176+
ARRAY_FILTER_USE_KEY
177+
);
178+
179+
if (!empty($spec)) {
180+
$arguments = $spec;
181+
}
182+
}
183+
184+
return [$class, $arguments, $extends, (array) $implements];
185+
}
186+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<?php
2+
/**
3+
* CROSS PHPunit Utils
4+
*
5+
* @filesource
6+
* @copyright 2019 Cross Solution <https://www.cross-solution.de>
7+
* @license MIT
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace Cross\TestUtilsTest\TestCase;
13+
14+
use Cross\TestUtils\Exception\InvalidUsageException;
15+
16+
use Cross\TestUtils\TestCase\CreateProphecyTrait;
17+
18+
use Prophecy\Prophecy\ObjectProphecy;
19+
20+
/**
21+
* Tests for \Cross\TestUtils\TestCase\CreateProphecyTrait
22+
*
23+
* @covers \Cross\TestUtils\TestCase\CreateProphecyTrait
24+
* @author Mathias Gelhausen <gelhausen@cross-solution.de>
25+
*
26+
* @group Cross.TestUtils
27+
* @group Cross.TestUtils.TestCase
28+
* @group Cross.TestUtils.TestCase.CreateProphecyTrait
29+
*/
30+
class CreateProphecyTraitTest extends \PHPUnit_Framework_TestCase
31+
{
32+
private $target;
33+
34+
public function setup()
35+
{
36+
$this->target = new class
37+
{
38+
use CreateProphecyTrait;
39+
40+
public $prophecy;
41+
42+
public function prophesize($class)
43+
{
44+
$this->prophecy = new class($class) extends ObjectProphecy
45+
{
46+
public $class;
47+
public $called = [];
48+
49+
public function __construct($class)
50+
{
51+
$this->class = $class;
52+
}
53+
54+
public function __call($method, $args)
55+
{
56+
$this->called[$method][] = $args;
57+
return $this;
58+
}
59+
60+
public function willExtend($class)
61+
{
62+
return $this->__call(__FUNCTION__, [$class]);
63+
}
64+
65+
public function willImplement($interface)
66+
{
67+
return $this->__call(__FUNCTION__, [$interface]);
68+
}
69+
70+
public function willBeConstructedWith(
71+
array $arguments = null
72+
) {
73+
return $this->__call(__FUNCTION__, [$arguments]);
74+
}
75+
76+
public function reveal()
77+
{
78+
return $this->__call(__FUNCTION__, []);
79+
}
80+
};
81+
82+
return $this->prophecy;
83+
}
84+
};
85+
}
86+
87+
public function testThrowsExceptionIfInvalidSpecificationIsPassed()
88+
{
89+
$this->expectException(InvalidUsageException::class);
90+
$this->expectExceptionMessage('Expected string or array');
91+
92+
$this->target->createProphecy(new \stdClass);
93+
}
94+
95+
public function testThrowsExceptionIfFqcnNotFound()
96+
{
97+
$this->expectException(InvalidUsageException::class);
98+
$this->expectExceptionMessage('No FQCN found');
99+
100+
$this->target->createProphecy([]);
101+
}
102+
103+
public function testCreateSimpleProphecy()
104+
{
105+
$prophecy = $this->target->createProphecy(\stdClass::class);
106+
107+
static::assertEquals(\stdClass::class, $prophecy->class);
108+
}
109+
110+
public function testCreateProphecyFromSimpleArray()
111+
{
112+
$prophecy = $this->target->createProphecy([\stdClass::class, 'arg1']);
113+
114+
static::assertArrayHasKey('willBeConstructedWith', $prophecy->called);
115+
static::assertEquals([[1 => 'arg1']], $prophecy->called['willBeConstructedWith'][0]);
116+
}
117+
118+
public function testCreateProphecyFromArrayWithArguments()
119+
{
120+
$prophecy = $this->target->createProphecy([\stdClass::class, 'arguments' => ['arg1']]);
121+
122+
static::assertArrayHasKey('willBeConstructedWith', $prophecy->called);
123+
static::assertEquals([['arg1']], $prophecy->called['willBeConstructedWith'][0]);
124+
}
125+
126+
public function testCreateProphecyThatExtendsAndImplements()
127+
{
128+
$prophecy = $this->target->createProphecy([
129+
\stdClass::class,
130+
'extends' => \ArrayObject::class,
131+
'implements' => [\Serializable::class, \Countable::class]
132+
]);
133+
134+
static::assertArrayHasKey('willExtend', $prophecy->called);
135+
static::assertArrayHasKey('willImplement', $prophecy->called);
136+
static::assertEquals([\ArrayObject::class], $prophecy->called['willExtend'][0]);
137+
static::assertEquals([[\Serializable::class], [\Countable::class]], $prophecy->called['willImplement']);
138+
}
139+
140+
public function testCreateProphecyWithMethodPromises()
141+
{
142+
$prophecies = [
143+
['method1', 'willReturn' => 'test'],
144+
['method2', 'other' => ['arg1', 'arg2']]
145+
];
146+
147+
$prophecy = $this->target->createProphecy(\stdClass::class, $prophecies);
148+
149+
static::assertArrayHasKey('method1', $prophecy->called);
150+
static::assertArrayHasKey('willReturn', $prophecy->called);
151+
static::assertEquals(['test'], $prophecy->called['willReturn'][0]);
152+
153+
static::assertArrayHasKey('method2', $prophecy->called);
154+
static::assertArrayHasKey('other', $prophecy->called);
155+
static::assertEquals(['arg1', 'arg2'], $prophecy->called['other'][0]);
156+
}
157+
158+
public function testCreateDouble()
159+
{
160+
$prophecy = $this->target->createDouble(\stdClass::class);
161+
162+
static::assertArrayHasKey('reveal', $prophecy->called);
163+
}
164+
}

0 commit comments

Comments
 (0)