diff --git a/src/Framework/MockObject/Runtime/Stub/ClosureMock.php b/src/Framework/MockObject/Runtime/Stub/ClosureMock.php new file mode 100644 index 00000000000..e2ef75be26e --- /dev/null +++ b/src/Framework/MockObject/Runtime/Stub/ClosureMock.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\Framework\MockObject\Stub; + +use PHPUnit\Framework\InvalidArgumentException; +use PHPUnit\Framework\MockObject\Builder\InvocationMocker; +use PHPUnit\Framework\MockObject\MethodCannotBeConfiguredException; +use PHPUnit\Framework\MockObject\MethodNameAlreadyConfiguredException; +use PHPUnit\Framework\MockObject\Rule\InvocationOrder; + +/** + * @mixin \PHPUnit\Framework\MockObject\MockObject + * + * @method InvocationMocker method($constraint) + */ +class ClosureMock +{ + public function __invoke(): mixed + { + return null; + } + + /** + * @throws InvalidArgumentException + * @throws MethodCannotBeConfiguredException + * @throws MethodNameAlreadyConfiguredException + */ + public function expectsClosure(InvocationOrder $invocationRule): InvocationMocker + { + return $this->expects($invocationRule) + ->method('__invoke'); + } + + public function closure(): InvocationMocker + { + return $this->method('__invoke'); + } +} diff --git a/src/Framework/TestCase.php b/src/Framework/TestCase.php index dd5fc99a96a..834e8bdfa50 100644 --- a/src/Framework/TestCase.php +++ b/src/Framework/TestCase.php @@ -78,6 +78,7 @@ use PHPUnit\Framework\MockObject\Rule\InvokedAtMostCount as InvokedAtMostCountMatcher; use PHPUnit\Framework\MockObject\Rule\InvokedCount as InvokedCountMatcher; use PHPUnit\Framework\MockObject\Stub; +use PHPUnit\Framework\MockObject\Stub\ClosureMock; use PHPUnit\Framework\MockObject\Stub\ConsecutiveCalls as ConsecutiveCallsStub; use PHPUnit\Framework\MockObject\Stub\Exception as ExceptionStub; use PHPUnit\Framework\MockObject\Stub\ReturnArgument as ReturnArgumentStub; @@ -1387,6 +1388,17 @@ final protected function createPartialMock(string $originalClassName, array $met return $partialMock; } + /** + * Creates mock of a closure. + * + * @throws InvalidArgumentException + * @throws MockObjectException + */ + final protected function createClosureMock(): ClosureMock|MockObject + { + return $this->createPartialMock(ClosureMock::class, ['__invoke']); + } + /** * Creates a test proxy for the specified class. * diff --git a/tests/unit/Framework/MockObject/Creation/CreateClosureMockTest.php b/tests/unit/Framework/MockObject/Creation/CreateClosureMockTest.php new file mode 100644 index 00000000000..e5fcad0cd13 --- /dev/null +++ b/tests/unit/Framework/MockObject/Creation/CreateClosureMockTest.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\Framework\MockObject; + +use function call_user_func_array; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Medium; +use PHPUnit\Framework\Attributes\TestDox; +use PHPUnit\Framework\ExpectationFailedException; +use PHPUnit\Framework\MockObject\Stub\ClosureMock; +use PHPUnit\Framework\TestCase; +use ReflectionProperty; + +#[Group('test-doubles')] +#[Group('test-doubles/creation')] +#[Group('test-doubles/mock-object')] +#[Medium] +#[TestDox('createClosureMock()')] +final class CreateClosureMockTest extends TestCase +{ + public function testCreateClosureMock(): void + { + $mock = $this->createClosureMock(); + + $this->assertInstanceOf(ClosureMock::class, $mock); + $this->assertInstanceOf(Stub::class, $mock); + } + + public function testCreateClosureMockWithReturnValue(): void + { + $mock = $this->createClosureMock(); + + $mock->closure()->willReturn(123); + + $this->assertSame(123, $mock()); + } + + public function testCreateClosureMockWithExpectation(): void + { + $mock = $this->createClosureMock(); + + $mock->expectsClosure($this->once()) + ->willReturn(123); + + $this->assertSame(123, $mock()); + } + + public function testClosureMockAppliesExpects(): void + { + $mock = $this->createClosureMock(); + + $mock->expectsClosure($this->once()); + + $this->assertThatMockObjectExpectationFails( + "Expectation failed for method name is \"__invoke\" when invoked 1 time.\nMethod was expected to be called 1 time, actually called 0 times.\n", + $mock, + ); + } + + private function assertThatMockObjectExpectationFails(string $expectationFailureMessage, MockObject $mock, string $methodName = '__phpunit_verify', array $arguments = []): void + { + try { + call_user_func_array([$mock, $methodName], $arguments); + } catch (ExpectationFailedException|MatchBuilderNotFoundException $e) { + $this->assertSame($expectationFailureMessage, $e->getMessage()); + + return; + } finally { + $this->resetMockObjects(); + } + + $this->fail(); + } + + private function resetMockObjects(): void + { + (new ReflectionProperty(TestCase::class, 'mockObjects'))->setValue($this, []); + } +}