PHPUnit:断言两个数组相等,但元素的顺序不重要

207

当数组元素的顺序无关紧要,甚至可能会发生改变时,断言两个对象数组相等的好方法是什么?


你关心数组中的对象是否相等,还是只关心两个数组中都有 x 个对象 y? - edorian
1
@edorian 两者都是最有趣的。但在我的情况下,每个数组中只有一个对象y。 - koen
@takeshin 等于符号 ==。在我的情况下,它们是值对象,因此相同并不是必要的。我可能可以创建一个自定义的断言方法。我需要的是计算每个数组中元素的数量,并且对于两个数组中的每个元素都必须存在相等(==)。 - koen
10
实际上,在PHPUnit 3.7.24版本中,$this->assertEquals函数会断言数组包含相同的键和值,而忽略它们的顺序。 - Dereckson
尝试使用此函数:https://dev59.com/1G025IYBdhLWcg3w_rFA#74435980 - devsmt
显示剩余2条评论
15个回答

342
你可以使用 assertEqualsCanonicalizing 方法,该方法在 PHPUnit 7.5 中添加。如果使用此方法比较数组,则这些数组将由 PHPUnit 数组比较器自行排序。
代码示例:
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;
    }
}

在旧版本的PHPUnit中,您可以使用assertEquals方法的一个未记录参数$canonicalize。如果您传递$canonicalize = true,您将获得相同的效果:

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;
    }
}

PHPUnit最新版本中的数组比较器源代码:https://github.com/sebastianbergmann/comparator/blob/master/src/ArrayComparator.php#L46


14
好的,为您翻译: "Fantastic. Why is this not the accepted answer, @koen?" 太好了。@koen,为什么这不是已被接受的答案呢? - rinogo
8
在函数中使用“$delta = 0.0,$maxDepth = 10,$canonicalize = true”作为参数传递是误导性的 - PHP 不支持命名参数。实际上这样做是设置了这三个变量,然后立即将它们的值传递给函数。如果这三个变量在局部范围内已经定义,则会导致问题,因为它们将被覆盖。 - Yi Jiang
14
@yi-jiang,这只是解释附加参数含义的最简洁方式。它比更简洁的变体更具自描述性:“$this->assertEquals($array1, $array2, "$canonicalize = true", 0.0, 10, true);”。我本可以用4行代替1行,但我没有这样做。 - pryazhnikov
16
这个解决方案会丢失钥匙,你没有指出这一点。 - Odalrick
8
请注意$canonicalize将被移除:https://github.com/sebastianbergmann/phpunit/issues/3342,`assertEqualsCanonicalizing()`将取而代之。 - koen
显示剩余4条评论

41

我的问题是我有两个数组(对我来说,数组键不相关,只关心值)。

例如,我想测试是否

$expected = array("0" => "green", "2" => "red", "5" => "blue", "9" => "pink");

与我无关的顺序相同的内容相同

$actual = array("0" => "pink", "1" => "green", "3" => "yellow", "red", "blue");

我使用了array_diff

最终的结果是(如果数组相等,则差异将导致空数组)。请注意,差异是双向计算的(感谢@beret,@GordonM)。

$this->assertEmpty(array_merge(array_diff($expected, $actual), array_diff($actual, $expected)));

如果要获得更加详细的错误信息(用于调试),你可以像这样测试(感谢@DenilsonSá):

$this->assertSame(array_diff($expected, $actual), array_diff($actual, $expected));

有缺陷的旧版本:

$this->assertEmpty(array_diff($array2, $array1));


这种方法的问题在于,如果$array1$array2具有更多的值,则即使数组值不相等,它也会返回空数组。您还应该测试数组大小是否相同,以确保一致。 - petrkotek
3
你应该使用array_diff或array_diff_assoc两种方式。如果一个数组是另一个数组的超集,那么单向的array_diff结果将为空,而另一方则非空。$a1 = [1,2,3,4,5]; $a2 = [1,3,5]; var_dump(array_diff($a1, $a2)); var_dump(array_diff($a2, $a1)) - GordonM
2
如果数组不为空,assertEmpty 不会打印出来,这在调试测试时非常不方便。我建议使用 $this->assertSame(array_diff($expected, $actual), array_diff($actual, $expected), $message);,因为这将以最少的额外代码打印出最有用的错误信息。这是有效的,因为 A\B = B\A ⇔ A\B 和 B\A 都为空 ⇔ A = B - Denilson Sá Maia
请注意,array_diff将每个值转换为字符串进行比较。 - Konstantin Pelepelin
补充@checat的回答:当你试图将数组转换为字符串时,你会收到一个“数组转换为字符串”的消息。解决这个问题的方法是使用“implode”。 - SameOldNick

40

最干净的方法是使用一个新的断言方法扩展phpunit。但现在有一个更简单的想法。以下是未经测试的代码,请验证:

在您的应用程序中的某个地方:

 /**
 * 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;
}

在您的测试中:
$this->assertTrue(arrays_are_similar($foo, $bar));

Craig,你接近我最初尝试的东西了。实际上,array_diff是我需要的,但似乎不适用于对象。我按照这里所述编写了自定义断言:http://www.phpunit.de/manual/current/en/extending-phpunit.html - koen
正确的链接现在是使用HTTPS而且没有WWW:https://phpunit.de/manual/current/en/extending-phpunit.html - Xavi Montero
1
foreach部分是不必要的 - array_diff_assoc已经比较了键和值。编辑:你还需要检查count(array_diff_assoc($b, $a)) - JohnSmith
鉴于php unit已经有本地支持(请参见下一个答案)...仍然可以将其实现为“扩展”phpunit..但这样做几乎总是错误的答案。 - ftrotter

28

另一种可能性:

  1. 对两个数组进行排序
  2. 将它们转换为字符串
  3. 断言两个字符串相等

$arr = array(23, 42, 108);
$exp = array(42, 23, 108);

sort($arr);
sort($exp);

$this->assertEquals(json_encode($exp), json_encode($arr));

如果任一数组包含对象,则json_encode仅编码公共属性。这仍然有效,但仅当决定相等性的所有属性都是公共的时才有效。请查看以下接口以控制私有属性的json编码。http://php.net/manual/en/class.jsonserializable.php - Westy92
1
这个即使不排序也能工作。对于 assertEquals,顺序并不重要。 - Wilt
1
实际上,我们也可以使用$this->assertSame($exp, $arr);进行比较,它与$this->assertEquals(json_encode($exp), json_encode($arr));的比较类似,唯一的区别是我们不必使用json_encode。 - maxwells

16

简单的帮助方法

protected function assertEqualsArrays($expected, $actual, $message) {
    $this->assertTrue(count($expected) == count(array_intersect($expected, $actual)), $message);
}

或者如果你需要在数组不相等时获取更多的调试信息

protected function assertEqualsArrays($expected, $actual, $message) {
    sort($expected);
    sort($actual);

    $this->assertEquals($expected, $actual, $message);
}

你还需要检查它是否与 count($actual) 相匹配,否则 assertEqualsArrays([], [1, 2, 3]) 将返回 true - Joseph Silber

9

如果键相同但顺序不同,这个方法可以解决问题。

只需要按照相同的顺序获取键并比较结果即可。

 /**
 * 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);
}

9
如果数组是可排序的,我会在检查相等之前对它们进行排序。如果不行,我会将它们转换为某种集合并比较它们。

9
即使您不关心顺序,考虑顺序可能会更容易:
尝试:
asort($foo);
asort($bar);
$this->assertEquals($foo, $bar);

8
使用 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)));

或者使用两个断言(更容易阅读):
// 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)));

那很聪明 :) - Christian
正是我所寻找的。简单易懂。 - Abdul Maye

7
我们在测试中使用以下包装器方法:

我们在测试中使用以下封装方法:

/**
 * 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);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接