What is a good way to assert that two arrays of objects are equal, when the order of the elements in the array is unimportant, or even subject to change?
-
Do you care about the objects in the array beeing equal or just that there are x amount of object y in both arrays ?edorian– edorian2010-10-01 10:39:00 +00:00Commented Oct 1, 2010 at 10:39
-
1@edorian Both would be most interesting. In my case though there is only one object y in each array.koen– koen2010-10-01 10:50:54 +00:00Commented Oct 1, 2010 at 10:50
-
please define equal. Is comparing sorted object hashes what do you need? You'll probably have to sort objects anyway.takeshin– takeshin2010-10-01 11:30:43 +00:00Commented Oct 1, 2010 at 11:30
-
@takeshin Equal as in ==. In my case they are value objects so sameness is not necessary. I probably could create a custom assert method. What I would need in it is count the number of elements in each array, and for each element in both on equal (==) must exist.koen– koen2010-10-01 13:29:48 +00:00Commented Oct 1, 2010 at 13:29
-
10Actually, on PHPUnit 3.7.24, $this->assertEquals asserts the array contains the same keys and values, disregarding in what order.Dereckson– Dereckson2014-02-09 05:46:13 +00:00Commented Feb 9, 2014 at 5:46
19 Answers
You can use assertEqualsCanonicalizing method which was added in PHPUnit 7.5. If you compare the arrays using this method, these arrays will be sorted by PHPUnit arrays comparator itself.
Code example:
class ArraysTest extends \PHPUnit\Framework\TestCase
{
public function testEquality()
{
$obj1 = $this->getObject(1);
$obj2 = $this->getObject(2);
$obj3 = $this->getObject(3);
$array1 = [$obj1, $obj2, $obj3];
$array2 = [$obj2, $obj1, $obj3];
// Pass
$this->assertEqualsCanonicalizing($array1, $array2);
// Fail
$this->assertEquals($array1, $array2);
}
private function getObject($value)
{
$result = new \stdClass();
$result->property = $value;
return $result;
}
}
In older versions of PHPUnit you can use an undocumented param $canonicalize of assertEquals method. If you pass $canonicalize = true, you will get the same effect:
class ArraysTest extends PHPUnit_Framework_TestCase
{
public function testEquality()
{
$obj1 = $this->getObject(1);
$obj2 = $this->getObject(2);
$obj3 = $this->getObject(3);
$array1 = [$obj1, $obj2, $obj3];
$array2 = [$obj2, $obj1, $obj3];
// Pass
$this->assertEquals($array1, $array2, "\$canonicalize = true", 0.0, 10, true);
// Fail
$this->assertEquals($array1, $array2, "Default behaviour");
}
private function getObject($value)
{
$result = new stdclass();
$result->property = $value;
return $result;
}
}
Arrays comparator source code at latest version of PHPUnit: https://github.com/sebastianbergmann/comparator/blob/master/src/ArrayComparator.php#L46
9 Comments
$delta = 0.0, $maxDepth = 10, $canonicalize = true to pass parameters into the function is misleading - PHP does not support named arguments. What this is actually doing is setting those three variables, then immediately passing their values to the function. This will cause problems if those three variables are already defined in the local scope since they will be overwritten.$this->assertEquals($array1, $array2, "\$canonicalize = true", 0.0, 10, true);. I could use 4 lines instead of 1, but I didn't do that.$canonicalize will be removed: github.com/sebastianbergmann/phpunit/issues/3342 and assertEqualsCanonicalizing() will replace it.My problem was that I had 2 arrays (array keys are not relevant for me, just the values).
For example I wanted to test if
$expected = array("0" => "green", "2" => "red", "5" => "blue", "9" => "pink");
had the same content (order not relevant for me) as
$actual = array("0" => "pink", "1" => "green", "3" => "yellow", "red", "blue");
So I have used array_diff.
Final result was (if the arrays are equal, the difference will result in an empty array). Please note that the difference is computed both ways (Thanks @beret, @GordonM)
$this->assertEmpty(array_merge(array_diff($expected, $actual), array_diff($actual, $expected)));
For a more detailed error message (while debugging), you can also test like this (thanks @DenilsonSá):
$this->assertSame(array_diff($expected, $actual), array_diff($actual, $expected));
Old version with bugs inside:
$this->assertEmpty(array_diff($array2, $array1));
5 Comments
$array1 has more values than $array2, then it returns empty array even though array values are not equal. You should also test, that array size is same, to be sure.$a1 = [1,2,3,4,5]; $a2 = [1,3,5]; var_dump (array_diff ($a1, $a2)); var_dump (array_diff ($a2, $a1))assertEmpty will not print the array if it is not empty, which is inconvenient while debugging tests. I'd suggest using: $this->assertSame(array_diff($expected, $actual), array_diff($actual, $expected), $message);, as this will print the most useful error message with the minimum of extra code. This works because A\B = B\A ⇔ A\B and B\A are empty ⇔ A = BArray to string conversion message when you try to cast an array to a string. A way to get around this is by using implodeThe cleanest way to do this would be to extend phpunit with a new assertion method. But here's an idea for a simpler way for now. Untested code, please verify:
Somewhere in your app:
/**
* Determine if two associative arrays are similar
*
* Both arrays must have the same indexes with identical values
* without respect to key ordering
*
* @param array $a
* @param array $b
* @return bool
*/
function arrays_are_similar($a, $b) {
// if the indexes don't match, return immediately
if (count(array_diff_assoc($a, $b))) {
return false;
}
// we know that the indexes, but maybe not values, match.
// compare the values between the two arrays
foreach($a as $k => $v) {
if ($v !== $b[$k]) {
return false;
}
}
// we have identical indexes, and no unequal values
return true;
}
In your test:
$this->assertTrue(arrays_are_similar($foo, $bar));
4 Comments
count(array_diff_assoc($b, $a)) also.One other possibility:
- Sort both arrays
- Convert them to a string
- Assert both strings are equal
$arr = array(23, 42, 108);
$exp = array(42, 23, 108);
sort($arr);
sort($exp);
$this->assertEquals(json_encode($exp), json_encode($arr));
3 Comments
assertEquals the order does not matter.$this->assertSame($exp, $arr); which does similar comparison as $this->assertEquals(json_encode($exp), json_encode($arr)); only difference is we don't have to use json_encodeSimple helper method
protected function assertEqualsArrays($expected, $actual, $message) {
$this->assertTrue(count($expected) == count(array_intersect($expected, $actual)), $message);
}
Or if you need more debug info when arrays are not equal
protected function assertEqualsArrays($expected, $actual, $message) {
$this->assertEquals(sort($expected), sort($actual), $message);
}
1 Comment
count($actual), otherwise assertEqualsArrays([], [1, 2, 3]) will return true.If the keys are the same but out of order this should solve it.
You just have to get the keys in the same order and compare the results.
/**
* Assert Array structures are the same
*
* @param array $expected Expected Array
* @param array $actual Actual Array
* @param string|null $msg Message to output on failure
*
* @return bool
*/
public function assertArrayStructure($expected, $actual, $msg = '') {
ksort($expected);
ksort($actual);
$this->assertSame($expected, $actual, $msg);
}
Comments
Using array_diff():
$a1 = array(1, 2, 3);
$a2 = array(3, 2, 1);
// error when arrays don't have the same elements (order doesn't matter):
$this->assertEquals(0, count(array_diff($a1, $a2)) + count(array_diff($a2, $a1)));
Or with 2 asserts (easier to read):
// error when arrays don't have the same elements (order doesn't matter):
$this->assertEquals(0, count(array_diff($a1, $a2)));
$this->assertEquals(0, count(array_diff($a2, $a1)));
2 Comments
We use the following wrapper method in our Tests:
/**
* Assert that two arrays are equal. This helper method will sort the two arrays before comparing them if
* necessary. This only works for one-dimensional arrays, if you need multi-dimension support, you will
* have to iterate through the dimensions yourself.
* @param array $expected the expected array
* @param array $actual the actual array
* @param bool $regard_order whether or not array elements may appear in any order, default is false
* @param bool $check_keys whether or not to check the keys in an associative array
*/
protected function assertArraysEqual(array $expected, array $actual, $regard_order = false, $check_keys = true) {
// check length first
$this->assertEquals(count($expected), count($actual), 'Failed to assert that two arrays have the same length.');
// sort arrays if order is irrelevant
if (!$regard_order) {
if ($check_keys) {
$this->assertTrue(ksort($expected), 'Failed to sort array.');
$this->assertTrue(ksort($actual), 'Failed to sort array.');
} else {
$this->assertTrue(sort($expected), 'Failed to sort array.');
$this->assertTrue(sort($actual), 'Failed to sort array.');
}
}
$this->assertEquals($expected, $actual);
}
Comments
The given solutions didn't do the job for me because I wanted to be able to handle multi-dimensional array and to have a clear message of what is different between the two arrays.
Here is my function
public function assertArrayEquals($array1, $array2, $rootPath = array())
{
foreach ($array1 as $key => $value)
{
$this->assertArrayHasKey($key, $array2);
if (isset($array2[$key]))
{
$keyPath = $rootPath;
$keyPath[] = $key;
if (is_array($value))
{
$this->assertArrayEquals($value, $array2[$key], $keyPath);
}
else
{
$this->assertEquals($value, $array2[$key], "Failed asserting that `".$array2[$key]."` matches expected `$value` for path `".implode(" > ", $keyPath)."`.");
}
}
}
}
Then to use it
$this->assertArrayEquals($array1, $array2, array("/"));
Comments
The problem with assertEqualsCanonicalizing() is that it can't be relied upon if you're comparing arrays with more complex structures and greater depth.
The best approach is to use the assertJsonStringEqualsJsonString function:
$this->assertJsonStringEqualsJsonString(
json_encode($arr1), // expected
json_encode($arr2), // actual
);
Benefits:
- Its clean
- Its part of PHPUnit (docs)
- Any depth can be used
- The order of the keys does not matter
- If there is a mismatch, it will show the difference (PhpStorm)
Lest see detailed explanation:
public function test_example(): void
{
// changing keys order -> OK
$this->assertJsonStringEqualsJsonString(
expectedJson: json_encode(['name' => 'Lukas', 'age' => 30]),
actualJson: json_encode(['age' => 30, 'name' => 'Lukas']),
);
// prepare data with greater depth
$expectedData = [
'name' => 'Lukas',
'age' => 30,
'relatives' => [
'spouse' => ['name' => 'Mary', 'age' => 29],
'siblings' => [
['name' => 'Adam', 'age' => 33],
['name' => 'Kasia', 'age' => 24],
],
],
];
// let's swap the keys, but without changing the meaning
$actualData = [
'relatives' => [
'siblings' => [
['age' => 33, 'name' => 'Adam'],
['age' => 24, 'name' => 'Kasia'],
],
'spouse' => ['age' => 29, 'name' => 'Mary'],
],
'age' => 30,
'name' => 'Lukas',
];
// nested comparasion -> OK
$this->assertJsonStringEqualsJsonString(
expectedJson: json_encode($expectedData),
actualJson: json_encode($actualData),
);
// is should be OK, but failed
// because `assertEqualsCanonicalizing` can't handle arrays of greater depth
//
// $this->assertEqualsCanonicalizing(
// expected: $expectedData,
// actual: $actualData,
// );
// and lastly lets check IDE difference
$actualData['name'] = 'Peter Parker';
$actualData['relatives']['spouse']['name'] = 'Mary Jane';
$this->assertJsonStringEqualsJsonString(
expectedJson: json_encode($expectedData),
actualJson: json_encode($actualData),
);
}
An example of viewing the difference in PhpStorm:
Comments
I wrote some simple code to first get all the keys from a multi-dimensional array:
/**
* Returns all keys from arrays with any number of levels
* @param array
* @return array
*/
protected function getAllArrayKeys($array)
{
$keys = array();
foreach ($array as $key => $element) {
$keys[] = $key;
if (is_array($array[$key])) {
$keys = array_merge($keys, $this->getAllArrayKeys($array[$key]));
}
}
return $keys;
}
Then to test that they were structured the same regardless of the order of keys:
$expectedKeys = $this->getAllArrayKeys($expectedData);
$actualKeys = $this->getAllArrayKeys($actualData);
$this->assertEmpty(array_diff($expectedKeys, $actualKeys));
HTH
Comments
If you have arrays of objects (so cannot use sort and thus not assertEqualsCanonicalizing), and only require that the result contains exactly the expected values (which are the same objects), in any order, regardless of keys, this helper method may be useful:
private static function assertArrayHasSameValues(
array $expected,
array $actual,
string $message = ''
): void {
self::assertCount(\count($expected), $actual, $message);
foreach ($expected as $expectedElement) {
self::assertContains($expectedElement, $actual, $message);
}
}
Comments
$this->assertEquals(json_encode($expectedData), json_encode($data), 'data should match');
3 Comments
I am converting to json and using Laravel assertJson():
$this->assertJson(json_encode($arrayOne), json_encode($arrayTwo));
1 Comment
assertJson only asserts that the string is a valid jsonIf values are just int or strings, and no multiple level arrays....
Why not just sorting the arrays, convert them to string...
$mapping = implode(',', array_sort($myArray));
$list = implode(',', array_sort($myExpectedArray));
... and then compare string:
$this->assertEquals($myExpectedArray, $myArray);
Comments
If you want test only the values of the array you can do:
$this->assertEquals(array_values($arrayOne), array_values($arrayTwo));
1 Comment
echo("<pre>"); print_r(array_values(array("size" => "XL", "color" => "gold"))); print_r(array_values(array("color" => "gold", "size" => "XL")));Another option, as if you didn't already have enough, is to combine assertArraySubset combined with assertCount to make your assertion. So, your code would look something like.
self::assertCount(EXPECTED_NUM_ELEMENT, $array);
self::assertArraySubset(SUBSET, $array);
This way you are order independent but still assert that all your elements are present.
2 Comments
assertArraySubset the order of the indexes matter so it will not work. i.e. self::assertArraySubset(['a'], ['b','a']) will be false, because [0 => 'a'] is not inside [0 => 'b', 1 => 'a']assertEquals already handles that if the keys are not in the same order. I just tested it.