204

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?

Valentin Despa
  • 40,712
  • 18
  • 80
  • 106
koen
  • 13,349
  • 10
  • 46
  • 51
  • 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 Oct 01 '10 at 10:39
  • 1
    @edorian Both would be most interesting. In my case though there is only one object y in each array. – koen Oct 01 '10 at 10:50
  • please define *equal*. Is comparing sorted [object hashes](http://php.net/manual/en/function.spl-object-hash.php) what do you need? You'll probably have to [sort objects](http://stackoverflow.com/questions/124266/sort-object-in-php) anyway. – takeshin Oct 01 '10 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 Oct 01 '10 at 13:29
  • [Comparator tools on PHPClasses](http://www.phpclasses.org/package/6496-PHP-Compare-and-sort-objects-of-many-types.html) – takeshin Oct 02 '10 at 22:55
  • 9
    Actually, on PHPUnit 3.7.24, $this->assertEquals asserts the array contains the same keys and values, disregarding in what order. – Dereckson Feb 09 '14 at 05:46
  • Try this function: https://stackoverflow.com/questions/5678959/php-check-if-two-arrays-are-equal/74435980#74435980 – devsmt Nov 15 '22 at 17:36

15 Answers15

338

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

pryazhnikov
  • 3,569
  • 2
  • 17
  • 17
  • 14
    Fantastic. Why is this not the accepted answer, @koen? – rinogo Jul 31 '15 at 02:59
  • 8
    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 Oct 15 '15 at 01:39
  • 14
    @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. – pryazhnikov Oct 15 '15 at 08:16
  • 15
    You don't point out that this solution will discard the keys. – Odalrick May 26 '16 at 08:03
  • 1
    When you don't want to use a undocumented feature you can do the same by calling: `sort($array1); sort($array2); $this->assertEquals($array1, $array2);` – Martijn Gastkemper Jan 26 '18 at 10:16
  • 1
    I'm with @YiJiang , this solution is correct but the example you give is confusing and could lead to other bugs within the test. You could improve the readability by slightly changing the error message: `$this->assertEquals($array1, $array2, "canonicalized contents did not match", 0.0, 10, true);` – adean Jul 17 '18 at 15:52
  • 8
    note that `$canonicalize` will be removed: https://github.com/sebastianbergmann/phpunit/issues/3342 and `assertEqualsCanonicalizing()` will replace it. – koen Oct 20 '18 at 21:09
  • 1
    What if the objects (such as `$obj1` etc) are associative arrays whose keys could change sort order? Is there a way to be agnostic not just at the top level but for the order of the keys of the associative arrays *within* that top level array? https://stackoverflow.com/q/57008999/470749 – Ryan Jul 12 '19 at 14:45
  • 1
    I've used this tip with some success, but in some cases (which I couldn't yet find a pattern for) it makes the test fail with a "Notice: Object of class ... could not be converted to int in ./vendor/sebastian/comparator/src/ArrayComparator.php:56.". Searching around, the only closest answer I could find was basically "don't do it with arrays of objects, it's wrong" [from the author himself](https://github.com/sebastianbergmann/comparator/issues/79#issuecomment-595641665) – Mismatch Jun 30 '21 at 16:42
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));

Mickäel A.
  • 9,012
  • 5
  • 54
  • 71
Valentin Despa
  • 40,712
  • 18
  • 80
  • 106
  • 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. – petrkotek Dec 16 '13 at 01:06
  • 3
    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))` – GordonM Mar 16 '14 at 10:04
  • 2
    `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* – Denilson Sá Maia Apr 14 '14 at 18:48
  • Note that array_diff converts every value to string for comparison. – Konstantin Pelepelin Jul 08 '14 at 12:35
  • 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`` – SameOldNick Dec 11 '16 at 17:46
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));
Craig
  • 706
  • 6
  • 8
  • 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: http://www.phpunit.de/manual/current/en/extending-phpunit.html – koen Oct 27 '10 at 10:29
  • Proper link now is with https and without www: https://phpunit.de/manual/current/en/extending-phpunit.html – Xavi Montero Jul 07 '17 at 22:37
  • 1
    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. – JohnSmith Feb 12 '18 at 15:57
  • 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. – ftrotter Aug 03 '21 at 23:49
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));
rodrigo-silveira
  • 12,607
  • 11
  • 69
  • 123
  • 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. http://php.net/manual/en/class.jsonserializable.php – Westy92 Sep 23 '15 at 02:20
  • 1
    This works even without sorting. For `assertEquals` the order does not matter. – Wilt Apr 21 '16 at 10:05
  • 1
    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 – maxwells Jun 26 '19 at 06:29
16

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) {
    sort($expected);
    sort($actual);

    $this->assertEquals($expected, $actual, $message);
}
ksimka
  • 1,394
  • 9
  • 21
  • You also have to check if it matches `count($actual)`, otherwise `assertEqualsArrays([], [1, 2, 3])` will return `true`. – Joseph Silber Jan 24 '21 at 16:41
9

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);
}
Zanshin13
  • 980
  • 4
  • 19
  • 39
Cris
  • 91
  • 1
  • 1
9

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.

Rodney Gitzel
  • 2,652
  • 16
  • 23
9

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);
Ruben Steins
  • 2,782
  • 4
  • 27
  • 48
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)));
caligari
  • 2,110
  • 20
  • 25
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);
}
theintz
  • 1,946
  • 1
  • 13
  • 21
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("/"));
moins52
  • 744
  • 8
  • 27
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

sturrockad
  • 4,460
  • 2
  • 19
  • 19
-1

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);
koalaok
  • 5,075
  • 11
  • 47
  • 91
-3

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.

Jonathan
  • 3,369
  • 4
  • 22
  • 27
  • 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']` – Robert T. Aug 30 '17 at 08:54
  • 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. – Cave Johnson Oct 31 '17 at 17:04
-3

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

$this->assertEquals(array_values($arrayOne), array_values($arrayTwo));
  • 1
    Unfortunately that is not testing "only the values" but both the values and the order of the values. E.g. `echo("
    ");
    print_r(array_values(array("size" => "XL", "color" => "gold")));
    print_r(array_values(array("color" => "gold", "size" => "XL")));`
    – Pocketsand Feb 11 '18 at 19:51