Skip to content

Commit 4550f89

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

File tree

4 files changed

+177
-11
lines changed

4 files changed

+177
-11
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: 116 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
*
@@ -41,10 +49,50 @@ class DocumentsMatchConstraint extends Constraint
4149
*/
4250
public function __construct($value, $ignoreExtraKeysInRoot = false, $ignoreExtraKeysInEmbedded = false, array $placeholders = [])
4351
{
52+
parent::__construct();
53+
4454
$this->value = $this->prepareBSON($value, true, $this->sortKeys);
4555
$this->ignoreExtraKeysInRoot = $ignoreExtraKeysInRoot;
4656
$this->ignoreExtraKeysInEmbedded = $ignoreExtraKeysInEmbedded;
4757
$this->placeholders = $placeholders;
58+
$this->comparatorFactory = Factory::getInstance();
59+
}
60+
61+
public function evaluate($other, $description = '', $returnResult = false)
62+
{
63+
/* TODO: If ignoreExtraKeys and sortKeys are both false, then we may be
64+
* able to skip preparation, convert both documents to extended JSON,
65+
* and compare strings.
66+
*
67+
* If ignoreExtraKeys is false and sortKeys is true, we still be able to
68+
* compare JSON strings but will still require preparation to sort keys
69+
* in all documents and sub-documents. */
70+
$other = $this->prepareBSON($other, true, $this->sortKeys);
71+
72+
$success = false;
73+
$this->lastFailure = null;
74+
75+
try {
76+
$this->assertEquals($this->value, $other, $this->ignoreExtraKeysInRoot);
77+
$success = true;
78+
} catch (RuntimeException $e) {
79+
$this->lastFailure = new ComparisonFailure(
80+
$this->value,
81+
$other,
82+
$this->exporter()->export($this->value),
83+
$this->exporter()->export($other),
84+
false,
85+
$e->getMessage()
86+
);
87+
}
88+
89+
if ($returnResult) {
90+
return $success;
91+
}
92+
93+
if (!$success) {
94+
$this->fail($other, $description, $this->lastFailure);
95+
}
4896
}
4997

5098
/**
@@ -53,19 +101,24 @@ public function __construct($value, $ignoreExtraKeysInRoot = false, $ignoreExtra
53101
* @param ArrayObject $expected
54102
* @param ArrayObject $actual
55103
* @param boolean $ignoreExtraKeys
104+
* @param string $keyPrefix
56105
* @throws RuntimeException if the documents do not match
57106
*/
58-
private function assertEquals(ArrayObject $expected, ArrayObject $actual, $ignoreExtraKeys)
107+
private function assertEquals(ArrayObject $expected, ArrayObject $actual, $ignoreExtraKeys, $keyPrefix = '')
59108
{
60109
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)));
110+
throw new RuntimeException(sprintf(
111+
'%s is not instance of expected class "%s"',
112+
$this->exporter()->shortenedExport($actual),
113+
get_class($expected)
114+
));
62115
}
63116

64117
foreach ($expected as $key => $expectedValue) {
65118
$actualHasKey = $actual->offsetExists($key);
66119

67120
if (!$actualHasKey) {
68-
throw new RuntimeException('$actual is missing key: ' . $key);
121+
throw new RuntimeException(sprintf('$actual is missing key: "%s"', $keyPrefix . $key));
69122
}
70123

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

77130
if (($expectedValue instanceof BSONArray && $actualValue instanceof BSONArray) ||
78131
($expectedValue instanceof BSONDocument && $actualValue instanceof BSONDocument)) {
79-
$this->assertEquals($expectedValue, $actualValue, $this->ignoreExtraKeysInEmbedded);
132+
$this->assertEquals($expectedValue, $actualValue, $this->ignoreExtraKeysInEmbedded, $keyPrefix . $key . '.');
80133
continue;
81134
}
82135

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

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

92186
foreach ($actual as $key => $value) {
93187
if (!$expected->offsetExists($key)) {
94-
throw new RuntimeException('$actual has extra key: ' . $key);
188+
throw new RuntimeException(sprintf('$actual has extra key: "%s"', $keyPrefix . $key));
95189
}
96190
}
97191
}
98192

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

119227
private function doToString()
120228
{
121-
return 'matches ' . json_encode($this->value);
229+
return 'matches ' . $this->exporter()->export($this->value);
122230
}
123231

124232
/**

tests/SpecTests/DocumentsMatchConstraintTest.php

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
namespace MongoDB\Tests\SpecTests;
44

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

710
class DocumentsMatchConstraintTest extends TestCase
811
{
@@ -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)