From adc23acd5e1b9f6a3fdd67d89d022b0905a1238c Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Sat, 19 Aug 2023 09:00:55 +0200 Subject: [PATCH] Closes #5478 --- .psalm/baseline.xml | 17 ++++ ChangeLog-9.6.md | 7 ++ src/Framework/Assert.php | 35 +++++++- src/Framework/Assert/Functions.php | 36 ++++++++ .../Constraint/Object/ObjectHasProperty.php | 84 +++++++++++++++++++ tests/unit/Framework/AssertTest.php | 32 +++++++ .../Constraint/ObjectHasPropertyTest.php | 63 ++++++++++++++ 7 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 src/Framework/Constraint/Object/ObjectHasProperty.php create mode 100644 tests/unit/Framework/Constraint/ObjectHasPropertyTest.php diff --git a/.psalm/baseline.xml b/.psalm/baseline.xml index eab468bb0bb..b9c17073564 100644 --- a/.psalm/baseline.xml +++ b/.psalm/baseline.xml @@ -178,6 +178,18 @@ static::objectEquals($expected, $method), $message, ) + static::assertThat( + $object, + new LogicalNot( + new ObjectHasProperty($propertyName), + ), + $message, + ) + static::assertThat( + $object, + new ObjectHasProperty($propertyName), + $message, + ) static::assertThat($haystack, $constraint, $message) static::assertThat($haystack, $constraint, $message) static::assertThat($haystack, $constraint, $message) @@ -437,6 +449,11 @@ hasProperty + + + hasProperty + + new static diff --git a/ChangeLog-9.6.md b/ChangeLog-9.6.md index 8e2313b3f55..d5930fb8f44 100644 --- a/ChangeLog-9.6.md +++ b/ChangeLog-9.6.md @@ -2,6 +2,12 @@ All notable changes of the PHPUnit 9.6 release series are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles. +## [9.6.11] - 2023-MM-DD + +### Added + +* [#5478](https://github.com/sebastianbergmann/phpunit/pull/5478): `assertObjectHasProperty()` and `assertObjectNotHasProperty()` + ## [9.6.10] - 2023-07-10 ### Changed @@ -77,6 +83,7 @@ All notable changes of the PHPUnit 9.6 release series are documented in this fil * [#5064](https://github.com/sebastianbergmann/phpunit/issues/5064): Deprecate `PHPUnit\Framework\TestCase::getMockClass()` * [#5132](https://github.com/sebastianbergmann/phpunit/issues/5132): Deprecate `Test` suffix for abstract test case classes +[9.6.11]: https://github.com/sebastianbergmann/phpunit/compare/9.6.10...9.6 [9.6.10]: https://github.com/sebastianbergmann/phpunit/compare/9.6.9...9.6.10 [9.6.9]: https://github.com/sebastianbergmann/phpunit/compare/9.6.8...9.6.9 [9.6.8]: https://github.com/sebastianbergmann/phpunit/compare/9.6.7...9.6.8 diff --git a/src/Framework/Assert.php b/src/Framework/Assert.php index c86a79a5dda..8724fae7b94 100644 --- a/src/Framework/Assert.php +++ b/src/Framework/Assert.php @@ -73,6 +73,7 @@ use PHPUnit\Framework\Constraint\LogicalXor; use PHPUnit\Framework\Constraint\ObjectEquals; use PHPUnit\Framework\Constraint\ObjectHasAttribute; +use PHPUnit\Framework\Constraint\ObjectHasProperty; use PHPUnit\Framework\Constraint\RegularExpression; use PHPUnit\Framework\Constraint\SameSize; use PHPUnit\Framework\Constraint\StringContains; @@ -1304,7 +1305,7 @@ public static function assertClassNotHasStaticAttribute(string $attributeName, s */ public static function assertObjectHasAttribute(string $attributeName, $object, string $message = ''): void { - self::createWarning('assertObjectHasAttribute() is deprecated and will be removed in PHPUnit 10. Refactor your test to use assertObjectHasProperty() (PHPUnit 10.1.0+) instead.'); + self::createWarning('assertObjectHasAttribute() is deprecated and will be removed in PHPUnit 10. Refactor your test to use assertObjectHasProperty() instead.'); if (!self::isValidObjectAttributeName($attributeName)) { throw InvalidArgumentException::create(1, 'valid attribute name'); @@ -1334,7 +1335,7 @@ public static function assertObjectHasAttribute(string $attributeName, $object, */ public static function assertObjectNotHasAttribute(string $attributeName, $object, string $message = ''): void { - self::createWarning('assertObjectNotHasAttribute() is deprecated and will be removed in PHPUnit 10. Refactor your test to use assertObjectNotHasProperty() (PHPUnit 10.1.0+) instead.'); + self::createWarning('assertObjectNotHasAttribute() is deprecated and will be removed in PHPUnit 10. Refactor your test to use assertObjectNotHasProperty() instead.'); if (!self::isValidObjectAttributeName($attributeName)) { throw InvalidArgumentException::create(1, 'valid attribute name'); @@ -1353,6 +1354,36 @@ public static function assertObjectNotHasAttribute(string $attributeName, $objec ); } + /** + * Asserts that an object has a specified property. + * + * @throws ExpectationFailedException + */ + final public static function assertObjectHasProperty(string $propertyName, object $object, string $message = ''): void + { + static::assertThat( + $object, + new ObjectHasProperty($propertyName), + $message, + ); + } + + /** + * Asserts that an object does not have a specified property. + * + * @throws ExpectationFailedException + */ + final public static function assertObjectNotHasProperty(string $propertyName, object $object, string $message = ''): void + { + static::assertThat( + $object, + new LogicalNot( + new ObjectHasProperty($propertyName), + ), + $message, + ); + } + /** * Asserts that two variables have the same type and value. * Used on objects, it asserts that two variables reference diff --git a/src/Framework/Assert/Functions.php b/src/Framework/Assert/Functions.php index 632d5c6fa1a..2005cfdebad 100644 --- a/src/Framework/Assert/Functions.php +++ b/src/Framework/Assert/Functions.php @@ -1444,6 +1444,42 @@ function assertObjectNotHasAttribute(string $attributeName, $object, string $mes } } +if (!function_exists('PHPUnit\Framework\assertObjectHasProperty')) { + /** + * Asserts that an object has a specified property. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit + * + * @see Assert::assertObjectHasProperty + */ + function assertObjectHasProperty(string $attributeName, object $object, string $message = ''): void + { + Assert::assertObjectHasProperty(...func_get_args()); + } +} + +if (!function_exists('PHPUnit\Framework\assertObjectNotHasProperty')) { + /** + * Asserts that an object does not have a specified property. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit + * + * @see Assert::assertObjectNotHasProperty + */ + function assertObjectNotHasProperty(string $attributeName, object $object, string $message = ''): void + { + Assert::assertObjectNotHasProperty(...func_get_args()); + } +} + if (!function_exists('PHPUnit\Framework\assertSame')) { /** * Asserts that two variables have the same type and value. diff --git a/src/Framework/Constraint/Object/ObjectHasProperty.php b/src/Framework/Constraint/Object/ObjectHasProperty.php new file mode 100644 index 00000000000..c41d21a142b --- /dev/null +++ b/src/Framework/Constraint/Object/ObjectHasProperty.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\Framework\Constraint; + +use function get_class; +use function gettype; +use function is_object; +use function sprintf; +use ReflectionObject; + +/** + * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit + */ +final class ObjectHasProperty extends Constraint +{ + /** + * @var string + */ + private $propertyName; + + public function __construct(string $propertyName) + { + $this->propertyName = $propertyName; + } + + /** + * Returns a string representation of the constraint. + */ + public function toString(): string + { + return sprintf( + 'has property "%s"', + $this->propertyName, + ); + } + + /** + * Evaluates the constraint for parameter $other. Returns true if the + * constraint is met, false otherwise. + * + * @param mixed $other value or object to evaluate + */ + protected function matches($other): bool + { + if (!is_object($other)) { + return false; + } + + return (new ReflectionObject($other))->hasProperty($this->propertyName); + } + + /** + * Returns the description of the failure. + * + * The beginning of failure messages is "Failed asserting that" in most + * cases. This method should return the second part of that sentence. + * + * @param mixed $other evaluated value or object + */ + protected function failureDescription($other): string + { + if (is_object($other)) { + return sprintf( + 'object of class "%s" %s', + get_class($other), + $this->toString(), + ); + } + + return sprintf( + '"%s" (%s) %s', + $other, + gettype($other), + $this->toString(), + ); + } +} diff --git a/tests/unit/Framework/AssertTest.php b/tests/unit/Framework/AssertTest.php index 64b040406a6..d083d15b3fc 100644 --- a/tests/unit/Framework/AssertTest.php +++ b/tests/unit/Framework/AssertTest.php @@ -2122,6 +2122,38 @@ public function testTwoObjectsCanBeAssertedToBeEqualUsingComparisonMethod(): voi $this->fail(); } + public function testObjectHasPropertyCanBeAsserted(): void + { + $objectWithProperty = new stdClass; + $objectWithProperty->theProperty = 'value'; + + $this->assertObjectHasProperty('theProperty', $objectWithProperty); + + try { + $this->assertObjectHasProperty('doesNotExist', $objectWithProperty); + } catch (AssertionFailedError $e) { + return; + } + + $this->fail(); + } + + public function testObjectDoesNotHavePropertyCanBeAsserted(): void + { + $objectWithProperty = new stdClass; + $objectWithProperty->theProperty = 'value'; + + $this->assertObjectNotHasProperty('doesNotExist', $objectWithProperty); + + try { + $this->assertObjectNotHasProperty('theProperty', $objectWithProperty); + } catch (AssertionFailedError $e) { + return; + } + + $this->fail(); + } + protected function sameValues(): array { $object = new SampleClass(4, 8, 15); diff --git a/tests/unit/Framework/Constraint/ObjectHasPropertyTest.php b/tests/unit/Framework/Constraint/ObjectHasPropertyTest.php new file mode 100644 index 00000000000..95b8a684652 --- /dev/null +++ b/tests/unit/Framework/Constraint/ObjectHasPropertyTest.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\Framework\Constraint; + +use PHPUnit\Framework\ExpectationFailedException; +use PHPUnit\Framework\TestCase; +use stdClass; + +/** + * @covers \PHPUnit\Framework\Constraint\ObjectHasProperty + * + * @small + */ +final class ObjectHasPropertyTest extends TestCase +{ + public function testCanBeEvaluated(): void + { + $constraint = new ObjectHasProperty('theProperty'); + + $objectWithProperty = new stdClass; + $objectWithProperty->theProperty = 'value'; + + $this->assertTrue($constraint->evaluate($objectWithProperty, '', true)); + $this->assertFalse($constraint->evaluate(new stdClass, '', true)); + $this->assertFalse($constraint->evaluate(null, '', true)); + + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('Failed asserting that object of class "stdClass" has property "theProperty".'); + + $constraint->evaluate(new stdClass); + } + + public function testHandlesNonObjectsGracefully(): void + { + $constraint = new ObjectHasProperty('theProperty'); + + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('Failed asserting that "non-object" (string) has property "theProperty".'); + + $constraint->evaluate('non-object'); + } + + public function testCanBeRepresentedAsString(): void + { + $constraint = new ObjectHasProperty('theProperty'); + + $this->assertSame('has property "theProperty"', $constraint->toString()); + } + + public function testIsCountable(): void + { + $constraint = new ObjectHasProperty('theProperty'); + + $this->assertCount(1, $constraint); + } +}