Skip to content

Commit 512dd4b

Browse files
committed
Allow mocking closures
Mocking closures is doable today by mocking __invoke on an invokable class. However this is troublesome, as it requires writing an invokable class, and mocking a class method. This new helper makes things easier via a new `$this->createClosureMock()` method.
1 parent 3ad902d commit 512dd4b

File tree

3 files changed

+124
-0
lines changed

3 files changed

+124
-0
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace PHPUnit\Framework\MockObject\Stub;
4+
5+
use PHPUnit\Framework\MockObject\Builder\InvocationMocker;
6+
use PHPUnit\Framework\MockObject\Rule\InvocationOrder;
7+
8+
class ClosureMock
9+
{
10+
public function __invoke()
11+
{
12+
}
13+
14+
public function expectsClosure(InvocationOrder $invocationRule): InvocationMocker
15+
{
16+
return $this->expects($invocationRule)
17+
->method('__invoke');
18+
}
19+
20+
public function closure(): InvocationMocker
21+
{
22+
return $this->method('__invoke');
23+
}
24+
}

src/Framework/TestCase.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
use PHPUnit\Framework\MockObject\Rule\InvokedAtMostCount as InvokedAtMostCountMatcher;
7979
use PHPUnit\Framework\MockObject\Rule\InvokedCount as InvokedCountMatcher;
8080
use PHPUnit\Framework\MockObject\Stub;
81+
use PHPUnit\Framework\MockObject\Stub\ClosureMock;
8182
use PHPUnit\Framework\MockObject\Stub\ConsecutiveCalls as ConsecutiveCallsStub;
8283
use PHPUnit\Framework\MockObject\Stub\Exception as ExceptionStub;
8384
use PHPUnit\Framework\MockObject\Stub\ReturnArgument as ReturnArgumentStub;
@@ -1387,6 +1388,17 @@ final protected function createPartialMock(string $originalClassName, array $met
13871388
return $partialMock;
13881389
}
13891390

1391+
/**
1392+
* Creates mock of a closure.
1393+
*
1394+
* @throws InvalidArgumentException
1395+
* @throws MockObjectException
1396+
*/
1397+
final protected function createClosureMock(): MockObject|ClosureMock
1398+
{
1399+
return $this->createPartialMock(ClosureMock::class, ['__invoke']);
1400+
}
1401+
13901402
/**
13911403
* Creates a test proxy for the specified class.
13921404
*
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of PHPUnit.
4+
*
5+
* (c) Sebastian Bergmann <[email protected]>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace PHPUnit\Framework\MockObject;
11+
12+
use PHPUnit\Framework\Attributes\Group;
13+
use PHPUnit\Framework\Attributes\Medium;
14+
use PHPUnit\Framework\Attributes\TestDox;
15+
use PHPUnit\Framework\ExpectationFailedException;
16+
use PHPUnit\Framework\MockObject\Generator\ClassIsFinalException;
17+
use PHPUnit\Framework\MockObject\Stub\ClosureMock;
18+
use PHPUnit\Framework\TestCase;
19+
use PHPUnit\TestFixture\MockObject\ExtendableClass;
20+
use PHPUnit\TestFixture\MockObject\InterfaceWithReturnTypeDeclaration;
21+
use ReflectionProperty;
22+
23+
#[Group('test-doubles')]
24+
#[Group('test-doubles/creation')]
25+
#[Group('test-doubles/mock-object')]
26+
#[Medium]
27+
#[TestDox('createClosureMock()')]
28+
final class CreateClosureMockTest extends TestCase
29+
{
30+
public function testCreateClosureMock(): void
31+
{
32+
$mock = $this->createClosureMock();
33+
34+
$this->assertInstanceOf(ClosureMock::class, $mock);
35+
$this->assertInstanceOf(Stub::class, $mock);
36+
}
37+
38+
public function testCreateClosureMockWithReturnValue(): void
39+
{
40+
$mock = $this->createClosureMock();
41+
42+
$mock->closure()->willReturn(123);
43+
44+
$this->assertSame(123, $mock());
45+
}
46+
47+
public function testCreateClosureMockWithExpectation(): void
48+
{
49+
$mock = $this->createClosureMock();
50+
51+
$mock->expectsClosure($this->once())
52+
->willReturn(123);
53+
54+
$this->assertSame(123, $mock());
55+
}
56+
57+
public function testClosureMockAppliesExpects(): void
58+
{
59+
$mock = $this->createClosureMock();
60+
61+
$mock->expectsClosure($this->once());
62+
63+
$this->assertThatMockObjectExpectationFails(
64+
"Expectation failed for method name is \"__invoke\" when invoked 1 time.\nMethod was expected to be called 1 time, actually called 0 times.\n",
65+
$mock,
66+
);
67+
}
68+
69+
private function assertThatMockObjectExpectationFails(string $expectationFailureMessage, MockObject $mock, string $methodName = '__phpunit_verify', array $arguments = []): void
70+
{
71+
try {
72+
call_user_func_array([$mock, $methodName], $arguments);
73+
} catch (ExpectationFailedException|MatchBuilderNotFoundException $e) {
74+
$this->assertSame($expectationFailureMessage, $e->getMessage());
75+
76+
return;
77+
} finally {
78+
$this->resetMockObjects();
79+
}
80+
81+
$this->fail();
82+
}
83+
84+
private function resetMockObjects(): void
85+
{
86+
(new ReflectionProperty(TestCase::class, 'mockObjects'))->setValue($this, []);
87+
}
88+
}

0 commit comments

Comments
 (0)