Skip to content

Commit da9fcd7

Browse files
committed
PHPLIB-459: Support better failure descriptions in DocumentsMatchConstraint
1 parent 7880bcd commit da9fcd7

File tree

4 files changed

+174
-10
lines changed

4 files changed

+174
-10
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
},
1818
"require-dev": {
1919
"phpunit/phpunit": "^5.7.27 || ^6.4 || ^8.3",
20+
"sebastian/comparator": "^1.0 || ^2.0 || ^3.0",
2021
"symfony/phpunit-bridge": "^4.4@dev"
2122
},
2223
"autoload": {

tests/SpecTests/DocumentsMatchConstraint.php

Lines changed: 114 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use ArrayObject;
99
use InvalidArgumentException;
1010
use RuntimeException;
11+
use SebastianBergmann\Comparator\ComparisonFailure;
12+
use SebastianBergmann\Comparator\Factory;
1113
use stdClass;
1214
use Symfony\Bridge\PhpUnit\ConstraintTrait;
1315

@@ -31,6 +33,12 @@ class DocumentsMatchConstraint extends Constraint
3133
private $sortKeys = false;
3234
private $value;
3335

36+
/** @var ComparisonFailure|null */
37+
private $lastFailure;
38+
39+
/** @var Factory */
40+
private $comparatorFactory;
41+
3442
/**
3543
* Creates a new constraint.
3644
*
@@ -45,6 +53,44 @@ public function __construct($value, $ignoreExtraKeysInRoot = false, $ignoreExtra
4553
$this->ignoreExtraKeysInRoot = $ignoreExtraKeysInRoot;
4654
$this->ignoreExtraKeysInEmbedded = $ignoreExtraKeysInEmbedded;
4755
$this->placeholders = $placeholders;
56+
$this->comparatorFactory = Factory::getInstance();
57+
}
58+
59+
public function evaluate($other, $description = '', $returnResult = false)
60+
{
61+
/* TODO: If ignoreExtraKeys and sortKeys are both false, then we may be
62+
* able to skip preparation, convert both documents to extended JSON,
63+
* and compare strings.
64+
*
65+
* If ignoreExtraKeys is false and sortKeys is true, we still be able to
66+
* compare JSON strings but will still require preparation to sort keys
67+
* in all documents and sub-documents. */
68+
$other = $this->prepareBSON($other, true, $this->sortKeys);
69+
70+
$success = false;
71+
$this->lastFailure = null;
72+
73+
try {
74+
$this->assertEquals($this->value, $other, $this->ignoreExtraKeysInRoot);
75+
$success = true;
76+
} catch (RuntimeException $e) {
77+
$this->lastFailure = new ComparisonFailure(
78+
$this->value,
79+
$other,
80+
$this->exporter()->export($this->value),
81+
$this->exporter()->export($other),
82+
false,
83+
$e->getMessage()
84+
);
85+
}
86+
87+
if ($returnResult) {
88+
return $success;
89+
}
90+
91+
if (!$success) {
92+
$this->fail($other, $description, $this->lastFailure);
93+
}
4894
}
4995

5096
/**
@@ -53,19 +99,24 @@ public function __construct($value, $ignoreExtraKeysInRoot = false, $ignoreExtra
5399
* @param ArrayObject $expected
54100
* @param ArrayObject $actual
55101
* @param boolean $ignoreExtraKeys
102+
* @param string $keyPrefix
56103
* @throws RuntimeException if the documents do not match
57104
*/
58-
private function assertEquals(ArrayObject $expected, ArrayObject $actual, $ignoreExtraKeys)
105+
private function assertEquals(ArrayObject $expected, ArrayObject $actual, $ignoreExtraKeys, $keyPrefix = '')
59106
{
60107
if (get_class($expected) !== get_class($actual)) {
61-
throw new RuntimeException(sprintf('$expected is %s but $actual is %s', get_class($expected), get_class($actual)));
108+
throw new RuntimeException(sprintf(
109+
'%s is not instance of expected class "%s"',
110+
$this->exporter()->shortenedExport($actual),
111+
get_class($expected)
112+
));
62113
}
63114

64115
foreach ($expected as $key => $expectedValue) {
65116
$actualHasKey = $actual->offsetExists($key);
66117

67118
if (!$actualHasKey) {
68-
throw new RuntimeException('$actual is missing key: ' . $key);
119+
throw new RuntimeException(sprintf('$actual is missing key: "%s"', $keyPrefix . $key));
69120
}
70121

71122
if (in_array($expectedValue, $this->placeholders, true)) {
@@ -76,12 +127,53 @@ private function assertEquals(ArrayObject $expected, ArrayObject $actual, $ignor
76127

77128
if (($expectedValue instanceof BSONArray && $actualValue instanceof BSONArray) ||
78129
($expectedValue instanceof BSONDocument && $actualValue instanceof BSONDocument)) {
79-
$this->assertEquals($expectedValue, $actualValue, $this->ignoreExtraKeysInEmbedded);
130+
$this->assertEquals($expectedValue, $actualValue, $this->ignoreExtraKeysInEmbedded, $keyPrefix . $key . '.');
80131
continue;
81132
}
82133

83-
if (gettype($expectedValue) != gettype($actualValue) || $expectedValue != $actualValue) {
84-
throw new RuntimeException('$expectedValue != $actualValue for key: ' . $key);
134+
if (is_scalar($expectedValue) && is_scalar($actualValue)) {
135+
if ($expectedValue !== $actualValue) {
136+
throw new ComparisonFailure(
137+
$expectedValue,
138+
$actualValue,
139+
'',
140+
'',
141+
false,
142+
sprintf('Field path "%s": %s', $keyPrefix . $key, 'Failed asserting that two values are equal.')
143+
);
144+
}
145+
146+
continue;
147+
}
148+
149+
// Workaround for ObjectComparator printing the whole actual object
150+
if (get_class($expectedValue) !== get_class($actualValue)) {
151+
throw new ComparisonFailure(
152+
$expectedValue,
153+
$actualValue,
154+
'',
155+
'',
156+
false,
157+
\sprintf(
158+
'Field path "%s": %s is not instance of expected class "%s".',
159+
$keyPrefix . $key,
160+
$this->exporter()->shortenedExport($actualValue),
161+
get_class($expectedValue)
162+
)
163+
);
164+
}
165+
166+
try {
167+
$this->comparatorFactory->getComparatorFor($expectedValue, $actualValue)->assertEquals($expectedValue, $actualValue);
168+
} catch (ComparisonFailure $failure) {
169+
throw new ComparisonFailure(
170+
$expectedValue,
171+
$actualValue,
172+
'',
173+
'',
174+
false,
175+
sprintf('Field path "%s": %s', $keyPrefix . $key, $failure->getMessage())
176+
);
85177
}
86178
}
87179

@@ -91,11 +183,25 @@ private function assertEquals(ArrayObject $expected, ArrayObject $actual, $ignor
91183

92184
foreach ($actual as $key => $value) {
93185
if (!$expected->offsetExists($key)) {
94-
throw new RuntimeException('$actual has extra key: ' . $key);
186+
throw new RuntimeException(sprintf('$actual has extra key: "%s"', $keyPrefix . $key));
95187
}
96188
}
97189
}
98190

191+
private function doAdditionalFailureDescription($other)
192+
{
193+
if ($this->lastFailure === null) {
194+
return '';
195+
}
196+
197+
return $this->lastFailure->getMessage();
198+
}
199+
200+
private function doFailureDescription($other)
201+
{
202+
return 'two BSON objects are equal';
203+
}
204+
99205
private function doMatches($other)
100206
{
101207
/* TODO: If ignoreExtraKeys and sortKeys are both false, then we may be
@@ -118,7 +224,7 @@ private function doMatches($other)
118224

119225
private function doToString()
120226
{
121-
return 'matches ' . json_encode($this->value);
227+
return 'matches ' . $this->exporter()->export($this->value);
122228
}
123229

124230
/**

tests/SpecTests/DocumentsMatchConstraintTest.php

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
namespace MongoDB\Tests\SpecTests;
44

5+
use ArrayObject;
6+
use MongoDB\Model\BSONArray;
7+
use PHPUnit\Framework\ExpectationFailedException;
58
use PHPUnit\Framework\TestCase;
69

710
class DocumentsMatchConstraintTest extends TestCase
@@ -16,7 +19,7 @@ public function testIgnoreExtraKeysInRoot()
1619
$this->assertResult(false, $c, ['x' => 1, 'y' => ['a' => 1, 'b' => 2, 'c' => 3]], 'Extra keys in embedded are not permitted');
1720
$this->assertResult(true, $c, ['y' => ['b' => 2, 'a' => 1], 'x' => 1], 'Root and embedded key order is not significant');
1821

19-
// Arrays are always intepretted as root documents
22+
// Arrays are always interpreted as root documents
2023
$c = new DocumentsMatchConstraint([1, ['a' => 1]], true, false);
2124

2225
$this->assertResult(false, $c, [1, 2], 'Incorrect value');
@@ -36,7 +39,7 @@ public function testIgnoreExtraKeysInEmbedded()
3639
$this->assertResult(true, $c, ['x' => 1, 'y' => ['a' => 1, 'b' => 2, 'c' => 3]], 'Extra keys in embedded are permitted');
3740
$this->assertResult(true, $c, ['y' => ['b' => 2, 'a' => 1], 'x' => 1], 'Root and embedded Key order is not significant');
3841

39-
// Arrays are always intepretted as root documents
42+
// Arrays are always interpreted as root documents
4043
$c = new DocumentsMatchConstraint([1, ['a' => 1]], false, true);
4144

4245
$this->assertResult(false, $c, [1, 2], 'Incorrect value');
@@ -55,6 +58,56 @@ public function testPlaceholders()
5558
$this->assertResult(true, $c, ['x' => '42', 'y' => 42, 'z' => ['a' => 24]], 'Exact match');
5659
}
5760

61+
/**
62+
* @dataProvider errorMessageProvider
63+
*/
64+
public function testErrorMessages($expectedMessagePart, DocumentsMatchConstraint $constraint, $actualValue)
65+
{
66+
try {
67+
$constraint->evaluate($actualValue);
68+
$this->fail('Expected a comparison failure');
69+
} catch (ExpectationFailedException $failure) {
70+
$this->assertStringContainsString('Failed asserting that two BSON objects are equal.', $failure->getMessage());
71+
$this->assertStringContainsString($expectedMessagePart, $failure->getMessage());
72+
}
73+
}
74+
75+
public function errorMessageProvider()
76+
{
77+
return [
78+
'Root type mismatch' => [
79+
'MongoDB\Model\BSONArray Object (...) is not instance of expected class "MongoDB\Model\BSONDocument"',
80+
new DocumentsMatchConstraint(['foo' => 'bar']),
81+
new BSONArray(['foo' => 'bar']),
82+
],
83+
'Missing key' => [
84+
'$actual is missing key: "foo.bar"',
85+
new DocumentsMatchConstraint(['foo' => ['bar' => 'baz']]),
86+
['foo' => ['foo' => 'bar']],
87+
],
88+
'Extra key' => [
89+
'$actual has extra key: "foo.foo"',
90+
new DocumentsMatchConstraint(['foo' => ['bar' => 'baz']]),
91+
['foo' => ['foo' => 'bar', 'bar' => 'baz']],
92+
],
93+
'Scalar value not equal' => [
94+
'Field path "foo": Failed asserting that two values are equal.',
95+
new DocumentsMatchConstraint(['foo' => 'bar']),
96+
['foo' => 'baz'],
97+
],
98+
'Scalar type mismatch' => [
99+
'Field path "foo": Failed asserting that two values are equal.',
100+
new DocumentsMatchConstraint(['foo' => 42]),
101+
['foo' => '42'],
102+
],
103+
'Type mismatch' => [
104+
'Field path "foo": MongoDB\Model\BSONDocument Object (...) is not instance of expected class "MongoDB\Model\BSONArray".',
105+
new DocumentsMatchConstraint(['foo' => ['bar']]),
106+
['foo' => (object) ['bar']],
107+
],
108+
];
109+
}
110+
58111
private function assertResult($expectedResult, DocumentsMatchConstraint $constraint, $value, $message)
59112
{
60113
$this->assertSame($expectedResult, $constraint->evaluate($value, '', true), $message);

tests/bootstrap.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,7 @@ class_alias(PHPUnit_Framework_Error_Warning::class, PHPUnit\Framework\Error\Warn
1717
if ( ! class_exists(PHPUnit\Framework\Constraint\Constraint::class)) {
1818
class_alias(PHPUnit_Framework_Constraint::class, PHPUnit\Framework\Constraint\Constraint::class);
1919
}
20+
21+
if ( ! class_exists(PHPUnit\Framework\ExpectationFailedException::class)) {
22+
class_alias(PHPUnit_Framework_ExpectationFailedException::class, PHPUnit\Framework\ExpectationFailedException::class);
23+
}

0 commit comments

Comments
 (0)