Skip to content

Commit d6537e8

Browse files
Merge pull request #199 from Yoast/feature/3.x/new-assertobjectnotequals-polyfill-trait
PHPUnit 11.2.0 | AssertObjectNotEquals trait: polyfill the Assert::assertObjectNotEquals() method
2 parents 9ffaffb + 35688cc commit d6537e8

13 files changed

+870
-131
lines changed

README.md

+11
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,17 @@ Refactoring tests which still use `Assert::assertArraySubset()` to use the new a
443443
[`Assert::assertArrayIsIdenticalToArrayOnlyConsideringListOfKeys()`]: https://docs.phpunit.de/en/main/assertions.html#assertarrayisidenticaltoarrayonlyconsideringlistofkeys
444444
[`Assert::assertArrayIsIdenticalToArrayIgnoringListOfKeys()`]: https://docs.phpunit.de/en/main/assertions.html#assertarrayisidenticaltoarrayignoringlistofkeys
445445

446+
#### PHPUnit < 11.2.0: `Yoast\PHPUnitPolyfills\Polyfills\AssertObjectNotEquals`
447+
448+
Polyfills the [`Assert::assertObjectNotEquals()`] method to verify two (value) objects are **_not_** considered equal.
449+
This is the sister-method to the PHPUnit 9.4+ `Assert::assertObjectEquals()` method.
450+
451+
This assertion expects an object to contain a comparator method in the object itself. This comparator method is subsequently called to verify the "equalness" of the objects.
452+
453+
The `assertObjectNotEquals()` assertion was introduced in PHPUnit 11.2.0.
454+
455+
[`Assert::assertObjectNotEquals()`]: https://docs.phpunit.de/en/main/assertions.html#assertobjectequals
456+
446457

447458
### TestCases
448459

phpunitpolyfills-autoload.php

+22
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ public static function load( $className ) {
8686
self::loadAssertArrayWithListKeys();
8787
return true;
8888

89+
case 'Yoast\PHPUnitPolyfills\Polyfills\AssertObjectNotEquals':
90+
self::loadAssertObjectNotEquals();
91+
return true;
92+
8993
case 'Yoast\PHPUnitPolyfills\TestCases\TestCase':
9094
self::loadTestCase();
9195
return true;
@@ -97,6 +101,7 @@ public static function load( $className ) {
97101
/*
98102
* Handles:
99103
* - Yoast\PHPUnitPolyfills\Exceptions\InvalidComparisonMethodException
104+
* - Yoast\PHPUnitPolyfills\Helpers\ComparatorValidator
100105
* - Yoast\PHPUnitPolyfills\Helpers\ResourceHelper
101106
* - Yoast\PHPUnitPolyfills\TestCases\XTestCase
102107
* - Yoast\PHPUnitPolyfills\TestListeners\TestListenerSnakeCaseMethods
@@ -334,6 +339,23 @@ public static function loadAssertArrayWithListKeys() {
334339
require_once __DIR__ . '/src/Polyfills/AssertArrayWithListKeys_Empty.php';
335340
}
336341

342+
/**
343+
* Load the AssertObjectNotEquals polyfill or an empty trait with the same name
344+
* if a PHPUnit version is used which already contains this functionality.
345+
*
346+
* @return void
347+
*/
348+
public static function loadAssertObjectNotEquals() {
349+
if ( \method_exists( Assert::class, 'assertObjectNotEquals' ) === false ) {
350+
// PHPUnit < 11.2.0.
351+
require_once __DIR__ . '/src/Polyfills/AssertObjectNotEquals.php';
352+
return;
353+
}
354+
355+
// PHPUnit >= 11.2.0.
356+
require_once __DIR__ . '/src/Polyfills/AssertObjectNotEquals_Empty.php';
357+
}
358+
337359
/**
338360
* Load the appropriate TestCase class based on the PHPUnit version being used.
339361
*

src/Exceptions/InvalidComparisonMethodException.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
use Exception;
66

77
/**
8-
* Exception used for all errors throw by the polyfill for the `assertObjectEquals()` assertion.
8+
* Exception used for all errors throw by the polyfill for the `assertObjectEquals()` and the `assertObjectNotEquals()` assertions.
99
*
1010
* PHPUnit natively throws a range of different exceptions.
11-
* The polyfill throws just one exception type with different messages.
11+
* The polyfills throw just one exception type with different messages.
1212
*/
1313
final class InvalidComparisonMethodException extends Exception {
1414

src/Helpers/ComparatorValidator.php

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
<?php
2+
3+
namespace Yoast\PHPUnitPolyfills\Helpers;
4+
5+
use ReflectionNamedType;
6+
use ReflectionObject;
7+
use ReflectionType;
8+
use Yoast\PHPUnitPolyfills\Exceptions\InvalidComparisonMethodException;
9+
10+
/**
11+
* Helper functions for validating a comparator method complies with the requirements set by PHPUnit.
12+
*
13+
* ---------------------------------------------------------------------------------------------
14+
* This class is only intended for internal use by PHPUnit Polyfills and is not part of the public API.
15+
* This also means that it has no promise of backward compatibility.
16+
*
17+
* End-users should use the {@see \Yoast\PHPUnitPolyfills\Polyfills\AssertObjectEquals} and/or the
18+
* {@see \Yoast\PHPUnitPolyfills\Polyfills\AssertObjectNotEquals} trait instead.
19+
* ---------------------------------------------------------------------------------------------
20+
*
21+
* @internal
22+
*/
23+
final class ComparatorValidator {
24+
25+
/**
26+
* Asserts that a custom object comparison method complies with the requirements set by PHPUnit.
27+
*
28+
* The custom comparator method is expected to have the following method
29+
* signature: `equals(self $other): bool` (or similar with a different method name).
30+
*
31+
* Basically, this method checks the following:
32+
* - A method with name $method must exist on the $actual object.
33+
* - The method must accept exactly one argument and this argument must be required.
34+
* - This parameter must have a classname-based declared type.
35+
* - The $expected object must be compatible with this declared type.
36+
* - The method must have a declared bool return type.
37+
*
38+
* {@internal Type validation for the parameters should be done in the calling function.}
39+
*
40+
* @param object $expected Expected value.
41+
* This object should comply with the type requirement set by the parameter type
42+
* of the comparator method on $actual.
43+
* @param object $actual The object on which the comparator method should exist.
44+
* @param string $method The name of the comparator method expected within the object.
45+
*
46+
* @return void
47+
*
48+
* @throws InvalidComparisonMethodException When the comparator method does not comply with the requirements.
49+
*/
50+
public static function isValid( $expected, $actual, $method = 'equals' ) {
51+
/*
52+
* Verify the method exists.
53+
*/
54+
$reflObject = new ReflectionObject( $actual );
55+
56+
if ( $reflObject->hasMethod( $method ) === false ) {
57+
throw new InvalidComparisonMethodException(
58+
\sprintf(
59+
'Comparison method %s::%s() does not exist.',
60+
\get_class( $actual ),
61+
$method
62+
)
63+
);
64+
}
65+
66+
$reflMethod = $reflObject->getMethod( $method );
67+
68+
/*
69+
* Comparator method return type requirements validation.
70+
*/
71+
$returnTypeError = \sprintf(
72+
'Comparison method %s::%s() does not declare bool return type.',
73+
\get_class( $actual ),
74+
$method
75+
);
76+
77+
if ( $reflMethod->hasReturnType() === false ) {
78+
throw new InvalidComparisonMethodException( $returnTypeError );
79+
}
80+
81+
$returnType = $reflMethod->getReturnType();
82+
83+
if ( \class_exists( 'ReflectionNamedType' ) ) {
84+
// PHP >= 7.1: guard against union/intersection return types.
85+
if ( ( $returnType instanceof ReflectionNamedType ) === false ) {
86+
throw new InvalidComparisonMethodException( $returnTypeError );
87+
}
88+
}
89+
elseif ( ( $returnType instanceof ReflectionType ) === false ) {
90+
/*
91+
* PHP 7.0.
92+
* Checking for `ReflectionType` will not throw an error on union types,
93+
* but then again union types are not supported on PHP 7.0.
94+
*/
95+
throw new InvalidComparisonMethodException( $returnTypeError );
96+
}
97+
98+
if ( $returnType->allowsNull() === true ) {
99+
throw new InvalidComparisonMethodException( $returnTypeError );
100+
}
101+
102+
if ( \method_exists( $returnType, 'getName' ) ) {
103+
// PHP >= 7.1.
104+
if ( $returnType->getName() !== 'bool' ) {
105+
throw new InvalidComparisonMethodException( $returnTypeError );
106+
}
107+
}
108+
elseif ( (string) $returnType !== 'bool' ) {
109+
// PHP 7.0.
110+
throw new InvalidComparisonMethodException( $returnTypeError );
111+
}
112+
113+
/*
114+
* Comparator method parameter requirements validation.
115+
*/
116+
if ( $reflMethod->getNumberOfParameters() !== 1
117+
|| $reflMethod->getNumberOfRequiredParameters() !== 1
118+
) {
119+
throw new InvalidComparisonMethodException(
120+
\sprintf(
121+
'Comparison method %s::%s() does not declare exactly one parameter.',
122+
\get_class( $actual ),
123+
$method
124+
)
125+
);
126+
}
127+
128+
$noDeclaredTypeError = \sprintf(
129+
'Parameter of comparison method %s::%s() does not have a declared type.',
130+
\get_class( $actual ),
131+
$method
132+
);
133+
134+
$notAcceptableTypeError = \sprintf(
135+
'%s is not an accepted argument type for comparison method %s::%s().',
136+
\get_class( $expected ),
137+
\get_class( $actual ),
138+
$method
139+
);
140+
141+
$reflParameter = $reflMethod->getParameters()[0];
142+
143+
$hasType = $reflParameter->hasType();
144+
if ( $hasType === false ) {
145+
throw new InvalidComparisonMethodException( $noDeclaredTypeError );
146+
}
147+
148+
$type = $reflParameter->getType();
149+
if ( \class_exists( 'ReflectionNamedType' ) ) {
150+
// PHP >= 7.1.
151+
if ( ( $type instanceof ReflectionNamedType ) === false ) {
152+
throw new InvalidComparisonMethodException( $noDeclaredTypeError );
153+
}
154+
155+
$typeName = $type->getName();
156+
}
157+
else {
158+
/*
159+
* PHP 7.0.
160+
* Checking for `ReflectionType` will not throw an error on union types,
161+
* but then again union types are not supported on PHP 7.0.
162+
*/
163+
if ( ( $type instanceof ReflectionType ) === false ) {
164+
throw new InvalidComparisonMethodException( $noDeclaredTypeError );
165+
}
166+
167+
$typeName = (string) $type;
168+
}
169+
170+
/*
171+
* Validate that the $expected object complies with the declared parameter type.
172+
*/
173+
if ( $typeName === 'self' ) {
174+
$typeName = \get_class( $actual );
175+
}
176+
177+
if ( ( $expected instanceof $typeName ) === false ) {
178+
throw new InvalidComparisonMethodException( $notAcceptableTypeError );
179+
}
180+
}
181+
}

0 commit comments

Comments
 (0)