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);
+ }
+}