Skip to content

Commit 5d9e3e2

Browse files
committed
PHPLIB-459: Support better failure descriptions in DocumentsMatchConstraint
1 parent 9074847 commit 5d9e3e2

File tree

4 files changed

+164
-27
lines changed

4 files changed

+164
-27
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"ext-mongodb": "^1.6"
1717
},
1818
"require-dev": {
19-
"phpunit/phpunit": "^5.7.27 || ^6.4"
19+
"phpunit/phpunit": "^5.7.27 || ^6.4",
20+
"sebastian/comparator": "^1.0 || ^2.0 || ^3.0"
2021
},
2122
"autoload": {
2223
"psr-4": { "MongoDB\\": "src/" },

tests/SpecTests/DocumentsMatchConstraint.php

Lines changed: 103 additions & 24 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

1315
/**
@@ -28,6 +30,12 @@ class DocumentsMatchConstraint extends Constraint
2830
private $sortKeys = false;
2931
private $value;
3032

33+
/** @var ComparisonFailure|null */
34+
private $lastFailure;
35+
36+
/** @var Factory */
37+
private $comparatorFactory;
38+
3139
/**
3240
* Creates a new constraint.
3341
*
@@ -43,26 +51,19 @@ public function __construct($value, $ignoreExtraKeysInRoot = false, $ignoreExtra
4351
$this->ignoreExtraKeysInRoot = $ignoreExtraKeysInRoot;
4452
$this->ignoreExtraKeysInEmbedded = $ignoreExtraKeysInEmbedded;
4553
$this->placeholders = $placeholders;
54+
$this->comparatorFactory = Factory::getInstance();
4655
}
4756

48-
/**
49-
* Returns a string representation of the constraint.
50-
*
51-
* @return string
52-
*/
53-
public function toString()
57+
protected function additionalFailureDescription($other)
5458
{
55-
return 'matches ' . json_encode($this->value);
59+
if ($this->lastFailure === null) {
60+
return '';
61+
}
62+
63+
return $this->lastFailure->getMessage();
5664
}
5765

58-
/**
59-
* Evaluates the constraint for parameter $other. Returns true if the
60-
* constraint is met, false otherwise.
61-
*
62-
* @param mixed $other
63-
* @return boolean
64-
*/
65-
protected function matches($other)
66+
public function evaluate($other, $description = '', $returnResult = false)
6667
{
6768
/* TODO: If ignoreExtraKeys and sortKeys are both false, then we may be
6869
* able to skip preparation, convert both documents to extended JSON,
@@ -73,13 +74,45 @@ protected function matches($other)
7374
* in all documents and sub-documents. */
7475
$other = $this->prepareBSON($other, true, $this->sortKeys);
7576

77+
$success = false;
78+
$this->lastFailure = null;
79+
7680
try {
7781
$this->assertEquals($this->value, $other, $this->ignoreExtraKeysInRoot);
82+
$success = true;
7883
} catch (RuntimeException $e) {
79-
return false;
84+
$this->lastFailure = new ComparisonFailure(
85+
$this->value,
86+
$other,
87+
$this->exporter->export($this->value),
88+
$this->exporter->export($other),
89+
false,
90+
$e->getMessage()
91+
);
8092
}
8193

82-
return true;
94+
if ($returnResult) {
95+
return $success;
96+
}
97+
98+
if (!$success) {
99+
$this->fail($other, $description, $this->lastFailure);
100+
}
101+
}
102+
103+
protected function failureDescription($other)
104+
{
105+
return 'two BSON objects are equal';
106+
}
107+
108+
/**
109+
* Returns a string representation of the constraint.
110+
*
111+
* @return string
112+
*/
113+
public function toString()
114+
{
115+
return 'matches ' . $this->exporter->export($this->value);
83116
}
84117

85118
/**
@@ -88,19 +121,24 @@ protected function matches($other)
88121
* @param ArrayObject $expected
89122
* @param ArrayObject $actual
90123
* @param boolean $ignoreExtraKeys
124+
* @param string $keyPrefix
91125
* @throws RuntimeException if the documents do not match
92126
*/
93-
private function assertEquals(ArrayObject $expected, ArrayObject $actual, $ignoreExtraKeys)
127+
private function assertEquals(ArrayObject $expected, ArrayObject $actual, $ignoreExtraKeys, $keyPrefix = '')
94128
{
95129
if (get_class($expected) !== get_class($actual)) {
96-
throw new RuntimeException(sprintf('$expected is %s but $actual is %s', get_class($expected), get_class($actual)));
130+
throw new RuntimeException(sprintf(
131+
'%s is not instance of expected class "%s"',
132+
$this->exporter->shortenedExport($actual),
133+
get_class($expected)
134+
));
97135
}
98136

99137
foreach ($expected as $key => $expectedValue) {
100138
$actualHasKey = $actual->offsetExists($key);
101139

102140
if (!$actualHasKey) {
103-
throw new RuntimeException('$actual is missing key: ' . $key);
141+
throw new RuntimeException(sprintf('$actual is missing key: "%s"', $keyPrefix . $key));
104142
}
105143

106144
if (in_array($expectedValue, $this->placeholders, true)) {
@@ -111,12 +149,53 @@ private function assertEquals(ArrayObject $expected, ArrayObject $actual, $ignor
111149

112150
if (($expectedValue instanceof BSONArray && $actualValue instanceof BSONArray) ||
113151
($expectedValue instanceof BSONDocument && $actualValue instanceof BSONDocument)) {
114-
$this->assertEquals($expectedValue, $actualValue, $this->ignoreExtraKeysInEmbedded);
152+
$this->assertEquals($expectedValue, $actualValue, $this->ignoreExtraKeysInEmbedded, $keyPrefix . $key . '.');
153+
continue;
154+
}
155+
156+
if (is_scalar($expectedValue) && is_scalar($actualValue)) {
157+
if ($expectedValue !== $actualValue) {
158+
throw new ComparisonFailure(
159+
$expectedValue,
160+
$actualValue,
161+
'',
162+
'',
163+
false,
164+
sprintf('Field path "%s": %s', $keyPrefix . $key, 'Failed asserting that two values are equal.')
165+
);
166+
}
167+
115168
continue;
116169
}
117170

118-
if (gettype($expectedValue) != gettype($actualValue) || $expectedValue != $actualValue) {
119-
throw new RuntimeException('$expectedValue != $actualValue for key: ' . $key);
171+
// Workaround for ObjectComparator printing the whole actual object
172+
if (get_class($expectedValue) !== get_class($actualValue)) {
173+
throw new ComparisonFailure(
174+
$expectedValue,
175+
$actualValue,
176+
'',
177+
'',
178+
false,
179+
\sprintf(
180+
'Field path "%s": %s is not instance of expected class "%s".',
181+
$keyPrefix . $key,
182+
$this->exporter->shortenedExport($actualValue),
183+
get_class($expectedValue)
184+
)
185+
);
186+
}
187+
188+
try {
189+
$this->comparatorFactory->getComparatorFor($expectedValue, $actualValue)->assertEquals($expectedValue, $actualValue);
190+
} catch (ComparisonFailure $failure) {
191+
throw new ComparisonFailure(
192+
$expectedValue,
193+
$actualValue,
194+
'',
195+
'',
196+
false,
197+
sprintf('Field path "%s": %s', $keyPrefix . $key, $failure->getMessage())
198+
);
120199
}
121200
}
122201

@@ -126,7 +205,7 @@ private function assertEquals(ArrayObject $expected, ArrayObject $actual, $ignor
126205

127206
foreach ($actual as $key => $value) {
128207
if (!$expected->offsetExists($key)) {
129-
throw new RuntimeException('$actual has extra key: ' . $key);
208+
throw new RuntimeException(sprintf('$actual has extra key: "%s"', $keyPrefix . $key));
130209
}
131210
}
132211
}

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->assertContains('Failed asserting that two BSON objects are equal.', $failure->getMessage());
71+
$this->assertContains($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)