From 6963b30c32aed4b210253d41588acabb6b0f66cb Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Mon, 16 Sep 2024 08:02:27 +0200 Subject: [PATCH] Support property hooks for extendable classes --- .php-cs-fixer.dist.php | 4 +- .../MockObject/Generator/Generator.php | 6 +- ...ExtendableClassWithPropertyWithGetHook.php | 19 ++++++ ...ExtendableClassWithPropertyWithSetHook.php | 19 ++++++ .../extendable_class_get_property_hook.phpt | 51 +++++++++++++++ ...xtendable_class_get_set_property_hook.phpt | 63 +++++++++++++++++++ .../extendable_class_set_property_hook.phpt | 51 +++++++++++++++ .../Framework/MockObject/MockObjectTest.php | 13 +++- .../MockObject/TestDoubleTestCase.php | 13 +++- 9 files changed, 235 insertions(+), 4 deletions(-) create mode 100644 tests/_files/mock-object/ExtendableClassWithPropertyWithGetHook.php create mode 100644 tests/_files/mock-object/ExtendableClassWithPropertyWithSetHook.php create mode 100644 tests/end-to-end/mock-objects/generator/extendable_class_get_property_hook.phpt create mode 100644 tests/end-to-end/mock-objects/generator/extendable_class_get_set_property_hook.phpt create mode 100644 tests/end-to-end/mock-objects/generator/extendable_class_set_property_hook.phpt diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 36060f3582..dbe3ac5425 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -14,7 +14,9 @@ ->in(__DIR__ . '/tests/_files') ->in(__DIR__ . '/tests/end-to-end') ->in(__DIR__ . '/tests/unit') - // InterfaceWithPropertyWithGetHook.php and InterfaceWithPropertyWithSetHook.php use PHP 8.4 syntax that currently leads to PHP-CS-Fixer errors + // *WithPropertyWith*Hook.php use PHP 8.4 syntax that currently leads to PHP-CS-Fixer errors + ->notName('ExtendableClassWithPropertyWithGetHook.php') + ->notName('ExtendableClassWithPropertyWithSetHook.php') ->notName('InterfaceWithPropertyWithGetHook.php') ->notName('InterfaceWithPropertyWithSetHook.php') // DeprecatedPhpFeatureTest.php must not use declare(strict_types=1); diff --git a/src/Framework/MockObject/Generator/Generator.php b/src/Framework/MockObject/Generator/Generator.php index ce789a4606..3af7ead714 100644 --- a/src/Framework/MockObject/Generator/Generator.php +++ b/src/Framework/MockObject/Generator/Generator.php @@ -768,12 +768,16 @@ private function generateCodeForTestDoubleClass(string $type, bool $mockObject, $doubledCloneMethod = true; } - if ($propertyHooksSupported && $isInterface) { + if ($propertyHooksSupported) { foreach ($class->getProperties() as $property) { if (!$property->isPublic()) { continue; } + /** + * @todo Do not generate doubled property when property is final + */ + /** @phpstan-ignore method.notFound */ $propertyHooks = $property->getHooks(); diff --git a/tests/_files/mock-object/ExtendableClassWithPropertyWithGetHook.php b/tests/_files/mock-object/ExtendableClassWithPropertyWithGetHook.php new file mode 100644 index 0000000000..dc5051f8d0 --- /dev/null +++ b/tests/_files/mock-object/ExtendableClassWithPropertyWithGetHook.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\TestFixture\MockObject; + +class ExtendableClassWithPropertyWithGetHook +{ + public string $property { + get { + return 'value'; + } + } +} diff --git a/tests/_files/mock-object/ExtendableClassWithPropertyWithSetHook.php b/tests/_files/mock-object/ExtendableClassWithPropertyWithSetHook.php new file mode 100644 index 0000000000..dd72d204a0 --- /dev/null +++ b/tests/_files/mock-object/ExtendableClassWithPropertyWithSetHook.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\TestFixture\MockObject; + +class ExtendableClassWithPropertyWithSetHook +{ + public string $property { + set (string $value) { + $this->property = $value; + } + } +} diff --git a/tests/end-to-end/mock-objects/generator/extendable_class_get_property_hook.phpt b/tests/end-to-end/mock-objects/generator/extendable_class_get_property_hook.phpt new file mode 100644 index 0000000000..0d39944317 --- /dev/null +++ b/tests/end-to-end/mock-objects/generator/extendable_class_get_property_hook.phpt @@ -0,0 +1,51 @@ +--TEST-- +Extendable class with property with non-final get property hook +--SKIPIF-- +generate( + Foo::class, + false, + false, + [], + 'TestStubFoo', +); + +print $testDoubleClass->classCode(); +--EXPECTF-- +declare(strict_types=1); + +class TestStubFoo extends Foo implements PHPUnit\Framework\MockObject\StubInternal +{ + use PHPUnit\Framework\MockObject\StubApi; + use PHPUnit\Framework\MockObject\GeneratedAsTestStub; + use PHPUnit\Framework\MockObject\Method; + use PHPUnit\Framework\MockObject\DoubledCloneMethod; + + public string $bar { + get { + return $this->__phpunit_getInvocationHandler()->invoke( + new \PHPUnit\Framework\MockObject\Invocation( + 'TestStubFoo', '$bar::get', [], 'string', $this, false + ) + ); + } + } +} diff --git a/tests/end-to-end/mock-objects/generator/extendable_class_get_set_property_hook.phpt b/tests/end-to-end/mock-objects/generator/extendable_class_get_set_property_hook.phpt new file mode 100644 index 0000000000..b2d9965c0a --- /dev/null +++ b/tests/end-to-end/mock-objects/generator/extendable_class_get_set_property_hook.phpt @@ -0,0 +1,63 @@ +--TEST-- +Extendable class with property with non-final get and set property hooks +--SKIPIF-- +bar = $value; + } + } +} + +require_once __DIR__ . '/../../../bootstrap.php'; + +$generator = new \PHPUnit\Framework\MockObject\Generator\Generator; + +$testDoubleClass = $generator->generate( + Foo::class, + false, + false, + [], + 'TestStubFoo', +); + +print $testDoubleClass->classCode(); +--EXPECTF-- +declare(strict_types=1); + +class TestStubFoo extends Foo implements PHPUnit\Framework\MockObject\StubInternal +{ + use PHPUnit\Framework\MockObject\StubApi; + use PHPUnit\Framework\MockObject\GeneratedAsTestStub; + use PHPUnit\Framework\MockObject\Method; + use PHPUnit\Framework\MockObject\DoubledCloneMethod; + + public string $bar { + get { + return $this->__phpunit_getInvocationHandler()->invoke( + new \PHPUnit\Framework\MockObject\Invocation( + 'TestStubFoo', '$bar::get', [], 'string', $this, false + ) + ); + } + + set (string $value) { + $this->__phpunit_getInvocationHandler()->invoke( + new \PHPUnit\Framework\MockObject\Invocation( + 'TestStubFoo', '$bar::set', [$value], 'void', $this, false + ) + ); + } + } +} diff --git a/tests/end-to-end/mock-objects/generator/extendable_class_set_property_hook.phpt b/tests/end-to-end/mock-objects/generator/extendable_class_set_property_hook.phpt new file mode 100644 index 0000000000..090296c0a5 --- /dev/null +++ b/tests/end-to-end/mock-objects/generator/extendable_class_set_property_hook.phpt @@ -0,0 +1,51 @@ +--TEST-- +Extendable class with property with non-final set property hook +--SKIPIF-- +bar = $value; + } + } +} + +require_once __DIR__ . '/../../../bootstrap.php'; + +$generator = new \PHPUnit\Framework\MockObject\Generator\Generator; + +$testDoubleClass = $generator->generate( + Foo::class, + false, + false, + [], + 'TestStubFoo', +); + +print $testDoubleClass->classCode(); +--EXPECTF-- +declare(strict_types=1); + +class TestStubFoo extends Foo implements PHPUnit\Framework\MockObject\StubInternal +{ + use PHPUnit\Framework\MockObject\StubApi; + use PHPUnit\Framework\MockObject\GeneratedAsTestStub; + use PHPUnit\Framework\MockObject\Method; + use PHPUnit\Framework\MockObject\DoubledCloneMethod; + + public string $bar { + set (string $value) { + $this->__phpunit_getInvocationHandler()->invoke( + new \PHPUnit\Framework\MockObject\Invocation( + 'TestStubFoo', '$bar::set', [$value], 'void', $this, false + ) + ); + } + } +} diff --git a/tests/unit/Framework/MockObject/MockObjectTest.php b/tests/unit/Framework/MockObject/MockObjectTest.php index 7f385aca09..7089f9b279 100644 --- a/tests/unit/Framework/MockObject/MockObjectTest.php +++ b/tests/unit/Framework/MockObject/MockObjectTest.php @@ -20,6 +20,7 @@ use PHPUnit\Framework\MockObject\Runtime\PropertyHook; use PHPUnit\Framework\TestCase; use PHPUnit\TestFixture\MockObject\AnInterface; +use PHPUnit\TestFixture\MockObject\ExtendableClassWithPropertyWithSetHook; use PHPUnit\TestFixture\MockObject\InterfaceWithImplicitProtocol; use PHPUnit\TestFixture\MockObject\InterfaceWithPropertyWithSetHook; use PHPUnit\TestFixture\MockObject\InterfaceWithReturnTypeDeclaration; @@ -464,7 +465,7 @@ public function testExpectationsAreClonedWhenTestDoubleIsCloned(): void } #[RequiresPhp('^8.4')] - public function testExpectationThatSetHookForPropertyIsCalledOnceSucceedsWhenItIsCalledOnce(): void + public function testExpectationCanBeConfiguredForSetHookForPropertyOfInterface(): void { $double = $this->createTestDouble(InterfaceWithPropertyWithSetHook::class); @@ -473,6 +474,16 @@ public function testExpectationThatSetHookForPropertyIsCalledOnceSucceedsWhenItI $double->property = 'value'; } + #[RequiresPhp('^8.4')] + public function testExpectationCanBeConfiguredForSetHookForPropertyOfExtendableClass(): void + { + $double = $this->createTestDouble(ExtendableClassWithPropertyWithSetHook::class); + + $double->expects($this->once())->method(PropertyHook::set('property'))->with('value'); + + $double->property = 'value'; + } + /** * @param class-string $type */ diff --git a/tests/unit/Framework/MockObject/TestDoubleTestCase.php b/tests/unit/Framework/MockObject/TestDoubleTestCase.php index b0aacd4e98..980f90726f 100644 --- a/tests/unit/Framework/MockObject/TestDoubleTestCase.php +++ b/tests/unit/Framework/MockObject/TestDoubleTestCase.php @@ -18,6 +18,7 @@ use PHPUnit\Framework\TestCase; use PHPUnit\TestFixture\MockObject\ExtendableClassCallingMethodInDestructor; use PHPUnit\TestFixture\MockObject\ExtendableClassWithCloneMethod; +use PHPUnit\TestFixture\MockObject\ExtendableClassWithPropertyWithGetHook; use PHPUnit\TestFixture\MockObject\ExtendableReadonlyClassWithCloneMethod; use PHPUnit\TestFixture\MockObject\InterfaceWithMethodThatExpectsObject; use PHPUnit\TestFixture\MockObject\InterfaceWithMethodThatHasDefaultParameterValues; @@ -324,7 +325,7 @@ public function testDoubledMethodsCanBeCalledFromDestructorOnTestDoubleCreatedBy } #[RequiresPhp('^8.4')] - public function testGetHookForPropertyCanBeConfiguredToReturnValue(): void + public function testGetHookForPropertyOfInterfaceCanBeConfigured(): void { $double = $this->createTestDouble(InterfaceWithPropertyWithGetHook::class); @@ -333,6 +334,16 @@ public function testGetHookForPropertyCanBeConfiguredToReturnValue(): void $this->assertSame('value', $double->property); } + #[RequiresPhp('^8.4')] + public function testGetHookForPropertyOfExtendableClassCanBeConfigured(): void + { + $double = $this->createTestDouble(ExtendableClassWithPropertyWithGetHook::class); + + $double->method(PropertyHook::get('property'))->willReturn('value'); + + $this->assertSame('value', $double->property); + } + /** * @template RealInstanceType of object *