Skip to content

Commit a87171e

Browse files
committed
feat: add TestDouble trait
1 parent 248b2e9 commit a87171e

File tree

4 files changed

+407
-0
lines changed

4 files changed

+407
-0
lines changed

src/TestDouble.php

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kenjis\PhpUnitHelper;
6+
7+
use Closure;
8+
use PHPUnit\Framework\MockObject\Stub\Stub;
9+
10+
use function array_keys;
11+
use function array_merge;
12+
use function call_user_func_array;
13+
use function is_array;
14+
use function is_int;
15+
use function is_object;
16+
17+
trait TestDouble
18+
{
19+
/**
20+
* Get Mock Object
21+
*
22+
* $email = $this->getMockBuilder('CI_Email')
23+
* ->disableOriginalConstructor()
24+
* ->setMethods(['send'])
25+
* ->getMock();
26+
* $email->method('send')->willReturn(true);
27+
*
28+
* will be
29+
*
30+
* $email = $this->getDouble('CI_Email', ['send' => true]);
31+
*
32+
* @param class-string<object> $classname
33+
* @param array<string, mixed>|array<array> $params [method_name => return_value]
34+
* @param bool|array<mixed> $constructorParams false: disable constructor, array: constructor params
35+
*
36+
* @return mixed PHPUnit mock object
37+
*/
38+
public function getDouble(
39+
string $classname,
40+
array $params,
41+
$constructorParams = false
42+
) {
43+
// `disableOriginalConstructor()` is the default, because if we call
44+
// constructor, it may call `$this->load->...` or other CodeIgniter
45+
// methods in it. But we can't use them in
46+
// `$this->request->setCallablePreConstructor()`
47+
$mockBuilder = $this->getMockBuilder($classname);
48+
if ($constructorParams === false) {
49+
$mockBuilder->disableOriginalConstructor();
50+
} elseif (is_array($constructorParams)) {
51+
$mockBuilder->setConstructorArgs($constructorParams);
52+
}
53+
54+
$methods = [];
55+
$onConsecutiveCalls = [];
56+
$otherCalls = [];
57+
58+
foreach ($params as $key => $val) {
59+
if (is_int($key)) {
60+
$onConsecutiveCalls = array_merge($onConsecutiveCalls, $val);
61+
$methods[] = array_keys($val)[0];
62+
} else {
63+
$otherCalls[$key] = $val;
64+
$methods[] = $key;
65+
}
66+
}
67+
68+
$mock = $mockBuilder->setMethods($methods)->getMock();
69+
70+
foreach ($onConsecutiveCalls as $method => $returns) {
71+
$mock->expects($this->any())->method($method)
72+
->will(
73+
call_user_func_array(
74+
[$this, 'onConsecutiveCalls'],
75+
$returns
76+
)
77+
);
78+
}
79+
80+
foreach ($otherCalls as $method => $return) {
81+
if (
82+
is_object(
83+
$return
84+
) && ($return instanceof Stub)
85+
) {
86+
$mock->expects($this->any())->method($method)
87+
->will($return);
88+
} elseif (is_object($return) && $return instanceof Closure) {
89+
$mock->expects($this->any())->method($method)
90+
->willReturnCallback($return);
91+
} else {
92+
$mock->expects($this->any())->method($method)
93+
->willReturn($return);
94+
}
95+
}
96+
97+
return $mock;
98+
}
99+
100+
/**
101+
* @param array<mixed>|null $params
102+
* @param mixed $expects
103+
*/
104+
protected function verify(
105+
object $mock,
106+
string $method,
107+
?array $params,
108+
$expects,
109+
string $with
110+
): void {
111+
$invocation = $mock->expects($expects)->method($method);
112+
113+
if ($params === null) {
114+
return;
115+
}
116+
117+
call_user_func_array([$invocation, $with], $params);
118+
}
119+
120+
/**
121+
* Verifies that method was called exactly $times times
122+
*
123+
* $loader->expects($this->exactly(2))
124+
* ->method('view')
125+
* ->withConsecutive(
126+
* ['shop_confirm', $this->anything(), true],
127+
* ['shop_tmpl_checkout', $this->anything()]
128+
* );
129+
*
130+
* will be
131+
*
132+
* $this->verifyInvokedMultipleTimes(
133+
* $loader,
134+
* 'view',
135+
* 2,
136+
* [
137+
* ['shop_confirm', $this->anything(), true],
138+
* ['shop_tmpl_checkout', $this->anything()]
139+
* ]
140+
* );
141+
*
142+
* @param object $mock PHPUnit mock object
143+
* @param array<mixed> $params arguments
144+
*/
145+
public function verifyInvokedMultipleTimes(
146+
object $mock,
147+
string $method,
148+
int $times,
149+
?array $params = null
150+
): void {
151+
$this->verify(
152+
$mock,
153+
$method,
154+
$params,
155+
$this->exactly($times),
156+
'withConsecutive'
157+
);
158+
}
159+
160+
/**
161+
* Verifies a method was invoked at least once
162+
*
163+
* @param object $mock PHPUnit mock object
164+
* @param array<mixed> $params arguments
165+
*/
166+
public function verifyInvoked(
167+
object $mock,
168+
string $method,
169+
?array $params = null
170+
): void {
171+
$this->verify(
172+
$mock,
173+
$method,
174+
$params,
175+
$this->atLeastOnce(),
176+
'with'
177+
);
178+
}
179+
180+
/**
181+
* Verifies that method was invoked only once
182+
*
183+
* @param object $mock PHPUnit mock object
184+
* @param array<mixed> $params arguments
185+
*/
186+
public function verifyInvokedOnce(
187+
object $mock,
188+
string $method,
189+
?array $params = null
190+
): void {
191+
$this->verify(
192+
$mock,
193+
$method,
194+
$params,
195+
$this->once(),
196+
'with'
197+
);
198+
}
199+
200+
/**
201+
* Verifies that method was not called
202+
*
203+
* @param object $mock PHPUnit mock object
204+
* @param array<mixed> $params arguments
205+
*/
206+
public function verifyNeverInvoked(
207+
object $mock,
208+
string $method,
209+
?array $params = null
210+
): void {
211+
$this->verify(
212+
$mock,
213+
$method,
214+
$params,
215+
$this->never(),
216+
'with'
217+
);
218+
}
219+
}

tests/Fake/CI_Email.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kenjis\PhpUnitHelper;
6+
7+
class CI_Email
8+
{
9+
}

tests/Fake/CI_Input.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kenjis\PhpUnitHelper;
6+
7+
class CI_Input
8+
{
9+
public function method(): string
10+
{
11+
return 'GET';
12+
}
13+
}

0 commit comments

Comments
 (0)