221

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?

7
  • Do you care about the objects in the array beeing equal or just that there are x amount of object y in both arrays ? Commented 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. Commented 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. Commented 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. Commented Oct 1, 2010 at 13:29
  • 10
    Actually, on PHPUnit 3.7.24, $this->assertEquals asserts the array contains the same keys and values, disregarding in what order. Commented Feb 9, 2014 at 5:46

19 Answers 19

365

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

Sign up to request clarification or add additional context in comments.

9 Comments

Fantastic. Why is this not the accepted answer, @koen?
Using $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.
@yi-jiang, it's just the shortest way to explain the meaning of additional arguments. It's more self-descriptive then more clean variant: $this->assertEquals($array1, $array2, "\$canonicalize = true", 0.0, 10, true);. I could use 4 lines instead of 1, but I didn't do that.
You don't point out that this solution will discard the keys.
note that $canonicalize will be removed: github.com/sebastianbergmann/phpunit/issues/3342 and assertEqualsCanonicalizing() will replace it.
|
41

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

Issue of this approach is that if $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.
You should do the array_diff or array_diff_assoc both ways. If one array is a superset of the other then array_diff in one direction will be empty, but non-empty in the other. $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 = B
Note that array_diff converts every value to string for comparison.
To add to @checat: you will get a Array to string conversion message when you try to cast an array to a string. A way to get around this is by using implode
40

The 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

Craig, you're close to what I tried originally. Actually array_diff is what I needed, but it doesn't seem to work for objects. I did write my custom assertion as explained here: phpunit.de/manual/current/en/extending-phpunit.html
Proper link now is with https and without www: phpunit.de/manual/current/en/extending-phpunit.html
foreach part is unnecessary - array_diff_assoc already compares both keys and values. EDIT: and you need to check count(array_diff_assoc($b, $a)) also.
Given that there is native support in php unit (see the next answer down).. it is still possible to implement this as an "extension" phpunit.. but doing so is almost always the wrong answer.
28

One other possibility:

  1. Sort both arrays
  2. Convert them to a string
  3. 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

If either array contains objects, json_encode only encodes the public properties. This will still work, but only if all properties that determine equality are public. Take a look at the following interface to control json_encoding of private properties. php.net/manual/en/class.jsonserializable.php
This works even without sorting. For assertEquals the order does not matter.
Indeed, we can also use $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_encode
17

Simple 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

You also have to check if it matches count($actual), otherwise assertEqualsArrays([], [1, 2, 3]) will return true.
10

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

10

Even though you do not care about the order, it might be easier to take that into account:

Try:

asort($foo);
asort($bar);
$this->assertEquals($foo, $bar);

Comments

8

If the array is sortable, I would sort them both before checking equality. If not, I would convert them to sets of some sort and compare those.

Comments

8

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

That's smart :)
Exactly what I was looking for. Simple.
7

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

6

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

3

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:

  1. Its clean
  2. Its part of PHPUnit (docs)
  3. Any depth can be used
  4. The order of the keys does not matter
  5. 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:

PhpStorm Comparison Failure

Comments

2

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

0

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

-1
$this->assertEquals(json_encode($expectedData), json_encode($data), 'data should match');

3 Comments

Please add some explanation to your answer such that others can learn from it - especially when you duplicate an answer that is ten years old
Sorry did not see i've duplicated. it's just part of code which works in my code. I do assertion for exact match this way.
Feel free to remove your answer, or update it to contain more information
-1

I am converting to json and using Laravel assertJson():

$this->assertJson(json_encode($arrayOne), json_encode($arrayTwo));

1 Comment

The assertJson only asserts that the string is a valid json
-2

If 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

-3

If you want test only the values of the array you can do:

$this->assertEquals(array_values($arrayOne), array_values($arrayTwo));

1 Comment

Unfortunately that is not testing "only the values" but both the values and the order of the values. E.g. echo("<pre>"); print_r(array_values(array("size" => "XL", "color" => "gold"))); print_r(array_values(array("color" => "gold", "size" => "XL")));
-4

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

In 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']
Sorry but I have to concur with Robert. At first I thought that this would be a good solution to compare arrays with string keys, but assertEquals already handles that if the keys are not in the same order. I just tested it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.