PHPUnit: assert two arrays are equal, but order of elements not important
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?
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
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));
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));
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));
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);
}
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.
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);
}
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);
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)));
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);
}
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("/"));
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
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);
If you want test only the values of the array you can do:
$this->assertEquals(array_values($arrayOne), array_values($arrayTwo));
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.
精彩评论