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

Posted

技术标签:

【中文标题】PHPUnit:断言两个数组相等,但元素的顺序并不重要【英文标题】:PHPUnit: assert two arrays are equal, but order of elements not important 【发布时间】:2011-04-19 19:01:17 【问题描述】:

当数组中元素的顺序不重要甚至可能发生变化时,有什么好方法可以断言两个对象数组相等?

【问题讨论】:

您关心数组中的对象是否相等,还是只关心两个数组中有 x 个对象 y? @edorian 两者都是最有趣的。在我的情况下,虽然每个数组中只有一个对象 y。 请定义等于。比较排序object hashes你需要什么?无论如何,你可能不得不sort objects。 @takeshin 等于 ==。就我而言,它们是价值对象,因此不需要相同。我可能可以创建一个自定义断言方法。我需要的是计算每个数组中的元素数量,并且对于两个相等(==)中的每个元素都必须存在。 实际上,在 phpUnit 3.7.24 上,$this->assertEquals 断言数组包含相同的键和值,不考虑顺序。 【参考方案1】:

您可以使用 PHPUnit 7.5 中添加的 assertEqualsCanonicalizing 方法。如果使用这种方法比较数组,这些数组将由 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

【讨论】:

太棒了。为什么这不是公认的答案,@koen? 使用$delta = 0.0, $maxDepth = 10, $canonicalize = true 将参数传递给函数是一种误导——PHP 不支持命名参数。这实际上是在设置这三个变量,然后立即将它们的值传递给函数。如果这三个变量已经在本地范围内定义,这将导致问题,因为它们将被覆盖。 @yi-jiang,这只是解释附加参数含义的最短方法。它更具自我描述性,然后更简洁:$this->assertEquals($array1, $array2, "\$canonicalize = true", 0.0, 10, true);。我可以使用 4 行而不是 1 行,但我没有这样做。 你没有指出这个解决方案会丢弃密钥。 请注意 $canonicalize 将被删除:github.com/sebastianbergmann/phpunit/issues/3342 和 assertEqualsCanonicalizing() 将替换它。【参考方案2】:

最简洁的方法是使用新的断言方法扩展 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));

【讨论】:

克雷格,你和我最初尝试的很接近。实际上 array_diff 是我需要的,但它似乎不适用于对象。我确实按照此处的说明编写了自定义断言:phpunit.de/manual/current/en/extending-phpunit.html 正确的链接现在是带有 https 而没有 www:phpunit.de/manual/current/en/extending-phpunit.html foreach 部分是不必要的 - array_diff_assoc 已经比较了键和值。编辑:你还需要检查count(array_diff_assoc($b, $a)) 鉴于 php 单元有本机支持(请参阅下一个答案).. 仍然可以将其实现为“扩展”phpunit.. 但这样做几乎总是错误的答案.【参考方案3】:

我的问题是我有 2 个数组(数组键与我无关,只是值)。

比如我想测试一下

$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));

旧版本有bug:

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

【讨论】:

这种方法的问题是,如果$array1 的值多于$array2,那么即使数组值不相等,它也会返回空数组。您还应该测试一下,数组大小是否相同。 您应该同时使用 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)) assertEmpty 如果数组不为空,则不会打印该数组,这在调试测试时很不方便。我建议使用:$this->assertSame(array_diff($expected, $actual), array_diff($actual, $expected), $message);,因为这将以最少的额外代码打印最有用的错误消息。这是因为 A\B = B\A ⇔ A\B 和 B\A 是空的 ⇔ A = B 请注意,array_diff 会将每个值转换为字符串以进行比较。 添加到@checat:当您尝试将数组转换为字符串时,您将收到Array to string conversion 消息。解决此问题的一种方法是使用implode【参考方案4】:

另一种可能性:

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

$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_encoding。 php.net/manual/en/class.jsonserializable.php 即使没有排序也可以使用。对于assertEquals,顺序无关紧要。 事实上,我们也可以使用$this->assertSame($exp, $arr);$this->assertEquals(json_encode($exp), json_encode($arr)); 进行类似的比较,唯一的区别是我们不必使用json_encode【参考方案5】:

简单的辅助方法

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【参考方案6】:

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

【讨论】:

【参考方案7】:

如果键相同但乱序,这应该可以解决。

您只需按相同顺序获取密钥并比较结果即可。

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

【讨论】:

【参考方案8】:

即使您不关心订单,也可能更容易考虑到这一点:

试试:

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

【讨论】:

【参考方案9】:

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

或使用 2 个断言(更易于阅读):

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

【讨论】:

这很聪明:) 正是我想要的。很简单。【参考方案10】:

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

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

【讨论】:

【参考方案11】:

给定的解决方案不适合我,因为我希望能够处理多维数组并清楚地了解两个数组之间的不同之处。

这是我的功能

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)."`.");
            
        
    

然后使用它

$this->assertArrayEquals($array1, $array2, array("/"));

【讨论】:

【参考方案12】:

我写了一些简单的代码,首先从一个多维数组中获取所有的键:

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

然后测试它们的结构是否相同,无论键的顺序如何:

    $expectedKeys = $this->getAllArrayKeys($expectedData);
    $actualKeys = $this->getAllArrayKeys($actualData);
    $this->assertEmpty(array_diff($expectedKeys, $actualKeys));

HTH

【讨论】:

【参考方案13】:

如果值只是 int 或字符串,并且没有多级数组......

为什么不只是对数组进行排序,将它们转换为字符串...

    $mapping = implode(',', array_sort($myArray));

    $list = implode(',', array_sort($myExpectedArray));

...然后比较字符串:

    $this->assertEquals($myExpectedArray, $myArray);

【讨论】:

【参考方案14】:

如果您只想测试数组的值,您可以这样做:

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

【讨论】:

不幸的是,这不是测试“仅值”,而是测试值和值的顺序。例如。 echo("<pre>"); print_r(array_values(array("size" => "XL", "color" => "gold"))); print_r(array_values(array("color" => "gold", "size" => "XL")));【参考方案15】:

另一个选项,好像你还没有足够的,是结合assertArraySubset 结合assertCount 来做出你的断言。所以,你的代码看起来像。

self::assertCount(EXPECTED_NUM_ELEMENT, $array); self::assertArraySubset(SUBSET, $array);

这样你就可以独立于订单,但仍然断言你的所有元素都存在。

【讨论】:

assertArraySubset 中,索引的顺序很重要,因此它不起作用。即 self::assertArraySubset(['a'], ['b','a']) 将是错误的,因为 [0 => 'a'] 不在 [0 => 'b', 1 => 'a'] 抱歉,我必须同意罗伯特的观点。起初我认为这是比较数组与字符串键的一个很好的解决方案,但如果键的顺序不同,assertEquals 已经处理了。我刚刚测试过。

以上是关于PHPUnit:断言两个数组相等,但元素的顺序并不重要的主要内容,如果未能解决你的问题,请参考以下文章

PHPUnit中的嵌套数组键值断言

检查两个数组之间的相等性[重复]

将两个字典与 numpy 矩阵作为值进行比较

如果使用 Fluent Assertions 的顺序不同,如何断言两个列表不等价

PHPUnit:assertThat()是否增加断言计数?

在 PHPUnit 的 Selenium 扩展中使用 glob 来识别元素