Skip to content

Commit

Permalink
Support property hooks for extendable classes
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastianbergmann committed Sep 16, 2024
1 parent 97375eb commit 27266e7
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 4 deletions.
4 changes: 3 additions & 1 deletion .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 5 additions & 1 deletion src/Framework/MockObject/Generator/Generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* 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; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* 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; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
--TEST--
Extendable class with property with non-final get property hook
--SKIPIF--
<?php declare(strict_types=1);
if (!method_exists(ReflectionProperty::class, 'getHooks')) {
print 'skip: PHP 8.4 is required.';
}
--FILE--
<?php declare(strict_types=1);
class Foo
{
public string $bar {
get {
return '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
)
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
--TEST--
Extendable class with property with non-final get and set property hooks
--SKIPIF--
<?php declare(strict_types=1);
if (!method_exists(ReflectionProperty::class, 'getHooks')) {
print 'skip: PHP 8.4 is required.';
}
--FILE--
<?php declare(strict_types=1);
class Foo
{
public string $bar {
get {
return 'value';
}

set (string $value) {
$this->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
)
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
--TEST--
Extendable class with property with non-final set property hook
--SKIPIF--
<?php declare(strict_types=1);
if (!method_exists(ReflectionProperty::class, 'getHooks')) {
print 'skip: PHP 8.4 is required.';
}
--FILE--
<?php declare(strict_types=1);
class Foo
{
public string $bar {
set (string $value) {
$this->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
)
);
}
}
}
13 changes: 12 additions & 1 deletion tests/unit/Framework/MockObject/MockObjectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -464,7 +465,7 @@ public function testExpectationsAreClonedWhenTestDoubleIsCloned(): void
}

#[RequiresPhp('^8.4')]
public function testExpectationThatSetHookForPropertyIsCalledOnceSucceedsWhenItIsCalledOnce(): void
public function testExpectationCanBeConfiguredForSetHookForPropertyOfInterface(): void
{
$double = $this->createTestDouble(InterfaceWithPropertyWithSetHook::class);

Expand All @@ -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
*/
Expand Down
13 changes: 12 additions & 1 deletion tests/unit/Framework/MockObject/TestDoubleTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -324,7 +325,7 @@ public function testDoubledMethodsCanBeCalledFromDestructorOnTestDoubleCreatedBy
}

#[RequiresPhp('^8.4')]
public function testGetHookForPropertyCanBeConfiguredToReturnValue(): void
public function testGetHookForPropertyOfInterfaceCanBeConfigured(): void
{
$double = $this->createTestDouble(InterfaceWithPropertyWithGetHook::class);

Expand All @@ -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
*
Expand Down

0 comments on commit 27266e7

Please sign in to comment.