Skip to content

Commit c4de42b

Browse files
committed
Add SetupTargetTrait
1 parent 7277f0d commit c4de42b

2 files changed

Lines changed: 497 additions & 0 deletions

File tree

src/TestCase/SetupTargetTrait.php

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
<?php
2+
/**
3+
* CROSS PHPUnit Utils
4+
*
5+
* @filesource
6+
* @copyright 2019 Cross Solution <http://cross-solution.de>
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace Cross\TestUtils\TestCase;
12+
13+
use Cross\TestUtils\Exception\InvalidUsageException;
14+
use Cross\TestUtils\Utils\Instance;
15+
16+
/**
17+
* Setup the SUT from specifications.
18+
*
19+
* If the testcase using this trait provide its own setup() method,
20+
* it needs to call the setupTarget() method.
21+
*
22+
* @author Mathias Gelhausen <gelhausen@cross-solution.de>
23+
*/
24+
trait SetupTargetTrait
25+
{
26+
27+
public function setup()
28+
{
29+
$this->setupTarget();
30+
}
31+
32+
/**
33+
* Setup the SUT from specification
34+
*
35+
* You need to define the property _$target_.
36+
* This property holds the specification and will be set to the SUT.
37+
*
38+
* ### Examples
39+
*
40+
* 1. Create one SUT for all tests via the callback "initTarget":
41+
* _Read more about callbacks futher down._
42+
*
43+
* `$target = true;`
44+
*
45+
*
46+
* 2. Create one SUT for all tests from a specification for use with
47+
* {@link \Cross\TestUtils\Utils\Instance::create}:
48+
*
49+
* ```
50+
* $target = FQCN;
51+
* $target = [FQCN, argument, ...];
52+
* ```
53+
*
54+
* 3. If you want to create different SUTs for some tests, you need to
55+
* use the more verbose specification format:
56+
*
57+
* ```
58+
* $target = [
59+
* 'default' => [],
60+
* 'create' => [
61+
* 'for' => testnamePatterns,
62+
* 'target' => targetSpec,
63+
* 'reflection' => FQCN|bool,
64+
* 'callback' => callable,
65+
* 'arguments' => [argument, ...],
66+
* 'use' => presetName
67+
* ],
68+
* ];
69+
* ```
70+
*
71+
* * 'for' : Specify one or more (as array) patterns matching test names for which this
72+
* SUT specification should apply.
73+
* You may use '*' to match all test names starting with the string.
74+
* __Examples__:
75+
* * 'testCorrectBehaviour' : will match exactly the test name
76+
* * 'testCorrect*' : match all tests starting with "testCorrect"
77+
* * 'testWithProvider|#4': matches the test with dataset #4.
78+
* * 'testWith*|#2': matches all tests starting with testWith at dataset #2.
79+
* * '*': matches all tests. This is the default value assumed, if 'for' is not provided.
80+
* That means, it should always be the last entry, as all following entries are
81+
* ignored.
82+
*
83+
* * 'target' : specification for the target as understood by
84+
* {@link \Cross\TestUtils\Utils\Instance::create}
85+
*
86+
* * 'reflection' :
87+
* * _string_: Creates a \ReflectionClass from the FQCN
88+
* * _bool_: Enable or disable the creation of a reflection from
89+
* target specification (override default value)
90+
* For example you might want to create a reflection class
91+
* from specifications returned by a callback.
92+
*
93+
* * 'callback' : Specify a callback to be called, which should either return thr SUT instance
94+
* or a specification understood by {@link \Cross\TestUtils\Utils\Instance::withMappedArguments}
95+
*
96+
* * 'arguments' : Array of constructor arguments used to create the SUT.
97+
* Note: If target specification is an array, this arguments are ignored.
98+
*
99+
* * 'use': You can define presets to be used with a specification.
100+
* Presets are like the default values with a unique key name.
101+
* If use is present, the options from this preset key are merged in the
102+
* specification. (order: default -> preset -> spec (later merges override previous ones))
103+
*
104+
*
105+
* Default values:
106+
* Each test specification will be merged into the default values, if provided.
107+
*
108+
* @return void
109+
*/
110+
public function setupTarget(): void
111+
{
112+
if (!property_exists($this, 'target')) {
113+
return;
114+
}
115+
116+
if (!isset($this->target['create']) && !isset($this->target['default'])) {
117+
if (false === $this->target) {
118+
return;
119+
}
120+
$spec = true === $this->target ? ['callback' => 'initTarget'] : ['target' => $this->target];
121+
$this->target = $this->setupTargetInstance($spec);
122+
123+
return;
124+
}
125+
126+
$specs = $this->target['create'] ?? [];
127+
$nameParts = explode(' ', $this->getName());
128+
$name = reset($nameParts);
129+
$set = end($nameParts);
130+
$set = '#' == $set{0} || '"' == $set{0} ? trim($set, '"') : '';
131+
132+
foreach ($specs as $spec) {
133+
$for = isset($spec['for']) ? (array) $spec['for'] : ['*'];
134+
135+
foreach ($for as $pattern) {
136+
$search = false !== strpos($pattern, '|') ? "$name|$set" : $name;
137+
$pattern = str_replace(['*', '|'], ['.*', '\|'], $pattern);
138+
139+
if (preg_match('~^' . $pattern . '$~i', "$search")) {
140+
$defaultSpec = $this->target['default'] ?? [];
141+
$useSpec = isset($spec['use']) && isset($this->target[$spec['use']])
142+
? $this->target[$spec['use']]
143+
: []
144+
;
145+
$spec = array_merge($defaultSpec, $useSpec, $spec);
146+
$this->target = $this->setupTargetInstance($spec);
147+
return;
148+
}
149+
}
150+
}
151+
152+
$spec = $this->target['default'] ?? ['callback' => 'initTarget'];
153+
$this->target = $this->setupTargetInstance($spec);
154+
}
155+
156+
/**
157+
* Setup a SUT from specification.
158+
*
159+
* @param array $spec
160+
* @return object|null
161+
*/
162+
private function setupTargetInstance($spec): ?object
163+
{
164+
$reflection = false;
165+
$arguments = $spec['arguments'] ?? [];
166+
167+
if (isset($spec['reflection'])) {
168+
if (is_string($spec['reflection'])) {
169+
return Instance::reflection($spec['reflection']);
170+
}
171+
$reflection = (bool) $spec['reflection'];
172+
}
173+
174+
if (isset($spec['callback']) && false !== $spec['callback']) {
175+
if (!is_callable($spec['callback'])) {
176+
$spec['callback'] = [$this, $spec['callback']];
177+
178+
if (!is_callable($spec['callback'])) {
179+
throw InvalidUsageException::fromTrait(
180+
__TRAIT__,
181+
__CLASS__,
182+
'Invalid callback.'
183+
);
184+
}
185+
}
186+
187+
$target = $spec['callback']();
188+
189+
if (!is_object($target)) {
190+
return $reflection
191+
? Instance::reflection($target)
192+
: Instance::withMappedArguments($target, $arguments, $this);
193+
}
194+
195+
return $target;
196+
}
197+
198+
$target = $spec['target'] ?? false;
199+
200+
if (!$target) {
201+
return null;
202+
}
203+
204+
if ($reflection) {
205+
return Instance::reflection($target);
206+
}
207+
208+
return Instance::withMappedArguments($target, $arguments, $this);
209+
}
210+
}

0 commit comments

Comments
 (0)