1
|
|
|
<?php |
2
|
|
|
namespace Nopolabs\Test; |
3
|
|
|
|
4
|
|
|
use Closure; |
5
|
|
|
use PHPUnit\Framework\Exception; |
6
|
|
|
use PHPUnit_Framework_MockObject_Matcher_Invocation; |
7
|
|
|
use PHPUnit_Framework_MockObject_MockBuilder; |
8
|
|
|
use PHPUnit_Framework_MockObject_MockObject; |
9
|
|
|
use ReflectionClass; |
10
|
|
|
use ReflectionMethod; |
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* This trait expects to be used in a sub-class of PHPUnit\Framework\TestCase |
14
|
|
|
*/ |
15
|
|
|
trait MockWithExpectationsTrait |
16
|
|
|
{ |
17
|
|
|
// abstract methods implemented by PHPUnit\Framework\TestCase |
18
|
|
|
abstract public function getMockBuilder($className); |
19
|
|
|
abstract public static function any(); |
20
|
|
|
abstract public static function at($index); |
21
|
|
|
abstract public static function atLeast($requiredInvocations); |
22
|
|
|
abstract public static function atLeastOnce(); |
23
|
|
|
abstract public static function atMost($allowedInvocations); |
24
|
|
|
abstract public static function exactly($count); |
25
|
|
|
abstract public static function never(); |
26
|
|
|
abstract public static function once(); |
27
|
|
|
|
28
|
|
|
protected function newPartialMockWithExpectations( |
29
|
|
|
$className, |
30
|
|
|
array $expectations = [], |
31
|
|
|
array $constructorArgs = null): PHPUnit_Framework_MockObject_MockObject |
32
|
|
|
{ |
33
|
|
|
$methods = $this->getMethodsToMock($className, $expectations); |
34
|
|
|
$mock = $this->newPartialMock($className, $methods, $constructorArgs); |
35
|
|
|
|
36
|
|
|
if ($this->isAssociative($expectations)) { |
37
|
|
|
return $this->setExpectations($mock, $expectations); |
38
|
|
|
} |
39
|
|
|
|
40
|
|
|
return $this->setAtExpectations($mock, $expectations); |
41
|
|
|
} |
42
|
|
|
|
43
|
|
|
protected function newPartialMock( |
44
|
|
|
$className, |
45
|
|
|
array $methods = [], |
46
|
|
|
array $constructorArgs = null): PHPUnit_Framework_MockObject_MockObject |
47
|
|
|
{ |
48
|
|
|
/** @var PHPUnit_Framework_MockObject_MockBuilder $builder */ |
49
|
|
|
$builder = $this->getMockBuilder($className) |
50
|
|
|
->disableOriginalClone() |
51
|
|
|
->disableArgumentCloning() |
52
|
|
|
->disallowMockingUnknownTypes() |
53
|
|
|
->setMethods(empty($methods) ? null : $methods); |
54
|
|
|
|
55
|
|
|
if ($constructorArgs === null) { |
56
|
|
|
$builder->disableOriginalConstructor(); |
57
|
|
|
} else { |
58
|
|
|
$builder->setConstructorArgs($constructorArgs); |
59
|
|
|
} |
60
|
|
|
|
61
|
|
|
return $builder->getMock(); |
62
|
|
|
} |
63
|
|
|
|
64
|
|
|
protected function setExpectations( |
65
|
|
|
PHPUnit_Framework_MockObject_MockObject $mock, |
66
|
|
|
array $expectations |
67
|
|
|
) : PHPUnit_Framework_MockObject_MockObject |
68
|
|
|
{ |
69
|
|
|
foreach ($expectations as $method => $expectation) { |
70
|
|
|
if (!is_array($expectation)) { |
71
|
|
|
$expectation = ['invoked' => $expectation]; |
72
|
|
|
} |
73
|
|
|
$this->setExpectation($mock, $method, $expectation); |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
return $mock; |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
protected function setAtExpectations( |
80
|
|
|
PHPUnit_Framework_MockObject_MockObject $mock, |
81
|
|
|
array $atExpectations |
82
|
|
|
) : PHPUnit_Framework_MockObject_MockObject |
83
|
|
|
{ |
84
|
|
|
$index = 0; |
85
|
|
|
foreach ($atExpectations as $atExpectation) { |
86
|
|
|
array_push($atExpectation, []); |
87
|
|
|
list($method, $expectation) = $atExpectation; |
88
|
|
|
if ($expectation === 'never') { |
89
|
|
|
$mock->expects($this->never())->method($method); |
90
|
|
|
} elseif (is_array($expectation)) { |
91
|
|
|
$expectation['invoked'] = $this->at($index++); |
92
|
|
|
$this->setExpectation($mock, $method, $expectation); |
93
|
|
|
} else { |
94
|
|
|
throw new Exception("setAtExpectations cannot understand expectation '$expectation'"); |
95
|
|
|
} |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
return $mock; |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
protected function setExpectation( |
102
|
|
|
PHPUnit_Framework_MockObject_MockObject $mock, |
103
|
|
|
$method, |
104
|
|
|
array $expectation) |
105
|
|
|
{ |
106
|
|
|
$invoked = $this->getInvoked($expectation); |
107
|
|
|
$params = $expectation['params'] ?? null; |
108
|
|
|
$result = $expectation['result'] ?? null; |
109
|
|
|
$throws = $expectation['throws'] ?? null; |
110
|
|
|
|
111
|
|
|
if ($params !== null && !is_array($params)) { |
112
|
|
|
throw new Exception("expected params to be an array, got '$params'"); |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
if ($result !== null && $throws !== null) { |
116
|
|
|
throw new Exception("cannot expect both 'result' and 'throws'"); |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
$builder = $mock->expects($invoked)->method($method); |
120
|
|
|
|
121
|
|
|
if ($params !== null) { |
122
|
|
|
call_user_func_array([$builder, 'with'], $params); |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
if ($result !== null) { |
126
|
|
|
if ($result instanceof Closure) { |
127
|
|
|
$builder->willReturnCallback($result); |
128
|
|
|
} else { |
129
|
|
|
$builder->willReturn($result); |
130
|
|
|
} |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
if ($throws !== null) { |
134
|
|
|
$builder->willThrowException($throws); |
135
|
|
|
} |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
protected function getInvoked(array $expectation): PHPUnit_Framework_MockObject_Matcher_Invocation |
139
|
|
|
{ |
140
|
|
|
if (isset($expectation['invoked'])) { |
141
|
|
|
if ($expectation['invoked'] instanceof PHPUnit_Framework_MockObject_Matcher_Invocation) { |
142
|
|
|
$invoked = $expectation['invoked']; |
143
|
|
|
} else { |
144
|
|
|
$invoked = $this->convertToInvocation($expectation['invoked']); |
145
|
|
|
} |
146
|
|
|
} else { |
147
|
|
|
$invoked = $this->any(); |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
return $invoked; |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
protected function convertToInvocation($invoked): PHPUnit_Framework_MockObject_Matcher_Invocation |
154
|
|
|
{ |
155
|
|
|
if (is_numeric($invoked)) { |
156
|
|
|
return $this->convertNumeric($invoked); |
157
|
|
|
} |
158
|
|
|
if (preg_match("/(?'method'\w+)(?:\s+(?'count'\d+))?/", $invoked, $matches)) { |
159
|
|
|
if (isset($matches['count'])) { |
160
|
|
|
return $this->convertTwoWords($matches['method'], (int)$matches['count']); |
161
|
|
|
} |
162
|
|
|
return $this->convertOneWord($matches['method']); |
163
|
|
|
} |
164
|
|
|
throw new Exception("convertToMatcher cannot convert '$invoked'"); |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
protected function convertNumeric(int $times): PHPUnit_Framework_MockObject_Matcher_Invocation |
168
|
|
|
{ |
169
|
|
|
if ($times === 0) { |
170
|
|
|
return $this->never(); |
171
|
|
|
} |
172
|
|
|
if ($times === 1) { |
173
|
|
|
return $this->once(); |
174
|
|
|
} |
175
|
|
|
return $this->exactly($times); |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
protected function convertOneWord(string $method): PHPUnit_Framework_MockObject_Matcher_Invocation |
179
|
|
|
{ |
180
|
|
|
if ($method === 'once') { |
181
|
|
|
return $this->once(); |
182
|
|
|
} |
183
|
|
|
if ($method === 'any') { |
184
|
|
|
return $this->any(); |
185
|
|
|
} |
186
|
|
|
if ($method === 'never') { |
187
|
|
|
return $this->never(); |
188
|
|
|
} |
189
|
|
|
if ($method === 'atLeastOnce') { |
190
|
|
|
return $this->atLeastOnce(); |
191
|
|
|
} |
192
|
|
|
throw new Exception("convertOneWord cannot convert '$method'"); |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
protected function convertTwoWords(string $method, int $count): PHPUnit_Framework_MockObject_Matcher_Invocation |
196
|
|
|
{ |
197
|
|
|
if ($method === 'atLeast') { |
198
|
|
|
return $this->atLeast($count); |
199
|
|
|
} |
200
|
|
|
if ($method === 'exactly') { |
201
|
|
|
return $this->exactly($count); |
202
|
|
|
} |
203
|
|
|
if ($method === 'atMost') { |
204
|
|
|
return $this->atMost($count); |
205
|
|
|
} |
206
|
|
|
throw new Exception("convertTwoWords cannot convert '$method $count'"); |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
private function isAssociative(array $array): bool |
210
|
|
|
{ |
211
|
|
|
return array_keys($array) !== range(0, count($array) - 1); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
private function getMethodsToMock($className, array $expectations) |
215
|
|
|
{ |
216
|
|
|
$expectedMethods = $this->isAssociative($expectations) |
217
|
|
|
? array_unique(array_keys($expectations)) |
218
|
|
|
: array_unique(array_column($expectations, 0)); |
219
|
|
|
|
220
|
|
|
return $this->addMissingMethods($className, $expectedMethods); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
private function addMissingMethods($className, array $methods) : array |
224
|
|
|
{ |
225
|
|
|
$missingMethods = $this->getMissingMethods($className, $methods); |
226
|
|
|
foreach ($missingMethods as $method) { |
227
|
|
|
$methods[] = $method; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
return $methods; |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
private function getMissingMethods($className, array $methods) : array |
234
|
|
|
{ |
235
|
|
|
$reflection = new ReflectionClass($className); |
236
|
|
|
|
237
|
|
View Code Duplication |
if ($reflection->isInterface()) { |
|
|
|
|
238
|
|
|
$publicMethods = array_map(function (ReflectionMethod $method) { |
239
|
|
|
return $method->name; |
240
|
|
|
}, $reflection->getMethods(ReflectionMethod::IS_PUBLIC)); |
241
|
|
|
|
242
|
|
|
return array_diff($publicMethods, $methods); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
View Code Duplication |
if ($reflection->isAbstract()) { |
|
|
|
|
246
|
|
|
$abstractMethods = array_map(function(ReflectionMethod $method) { |
247
|
|
|
return $method->name; |
248
|
|
|
}, $reflection->getMethods(ReflectionMethod::IS_ABSTRACT)); |
249
|
|
|
|
250
|
|
|
return array_diff($abstractMethods, $methods); |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
return []; |
254
|
|
|
} |
255
|
|
|
} |
256
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.