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


132

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


您是否关心数组中的对象相等或只是两个数组中都有y个对象的x个?
edorian

@edorian两者都将是最有趣的。在我的情况下,尽管每个数组中只有一个对象y。
koen 2010年

请定义等于。比较排序的对象散列是否需要什么?无论如何,您可能都必须对对象进行排序
Takehin 2010年

@takeshin等于==。就我而言,它们是价值对象,因此没有必要相同。我可能可以创建一个自定义的assert方法。我需要的是计算每个数组中的元素数量,并且对于两个元素中的每个元素都必须存在相等(==)。
koen 2010年

7
实际上,在PHPUnit 3.7.24上,$ this-> assertEquals断言该数组包含相同的键和值,而无视顺序。
Dereckson 2014年

Answers:


38

最干净的方法是使用新的断言方法扩展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是我需要的,但它似乎不适用于对象。我确实按照我的解释写了我的自定义断言:phpunit.de/manual/current/en/extending-phpunit.html
koen

正确的链接现在带有https,而没有www:phpunit.de/manual/current/en/extending-phpunit.html
Xavi Montero

foreach部分是不必要的-array_diff_assoc已经比较了键和值。编辑:并且您还需要检查count(array_diff_assoc($b, $a))
JohnSmith

212

您可以使用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中,可以使用未声明的参数$ canonicalize assertEquals方法。如果传递$ 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


10
太棒了 为什么这不是公认的答案,@ koen?
rinogo

7
使用$delta = 0.0, $maxDepth = 10, $canonicalize = true将参数传递给函数会产生误导-PHP不支持命名参数。这实际上是在设置这三个变量,然后将它们的值立即传递给函数。如果这三个变量已经在本地范围内定义,则将导致问题,因为它们将被覆盖。
易江

11
@ yi-jiang,这只是解释附加参数含义的最短方法。它更自我描述则更加干净的变体:$this->assertEquals($array1, $array2, "\$canonicalize = true", 0.0, 10, true);。我可以使用4行而不是1行,但是我没有这样做。
pryazhnikov

8
您没有指出此解决方案将丢弃密钥。
奥达里克

8
注意,$canonicalize将被删除:github.com/sebastianbergmann/phpunit/issues/3342assertEqualsCanonicalizing()将取代它。
koen

35

我的问题是我有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));

包含错误的旧版本:

$ 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 2014年

2
assertEmpty如果数组不为空,则不会打印该数组,这在调试测试时很不方便。我建议使用:$this->assertSame(array_diff($expected, $actual), array_diff($actual, $expected), $message);,因为这将以最少的额外代码显示最有用的错误消息。之所以有效,是因为A \ B = B \ A⇔A \ B和B \ A为空⇔A = B
DenilsonSáMaia 2014年

请注意,array_diff将每个值转换为字符串以进行比较。
康斯坦丁·佩莱佩林

要添加到@checat:Array to string conversion尝试将数组强制转换为字符串时,您会收到一条消息。解决此问题的方法是使用implode
ub3rst4r

20

另一种可能性:

  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_encoding。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

15

简单的助手方法

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

8

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


6

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

这很聪明:)
Christian

正是我想要的。简单。
阿卜杜勒·梅


5

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

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

5

如果密钥相同但顺序混乱,则应解决该问题。

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

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

3

给定的解决方案对我来说没有用,因为我希望能够处理多维数组并清楚地知道两个数组之间的区别。

这是我的功能

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

1

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

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

高温超导


0

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

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

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

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

...然后比较字符串:

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

-2

如果只想测试数组的值,则可以执行以下操作:

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

1
不幸的是,这不是测试“仅值”,而是测试值和值的顺序。例如echo("<pre>"); print_r(array_values(array("size" => "XL", "color" => "gold"))); print_r(array_values(array("color" => "gold", "size" => "XL")));
Pocketsand,

-3

好像您还没有足够的选择,另一种选择是assertArraySubset结合使用assertCount和进行断言。因此,您的代码看起来像。

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

这样,您是顺序独立的,但仍断言所有元素都存在。


assertArraySubset该索引的顺序关系,因此将无法正常工作。即self :: assertArraySubset(['a'],['b','a'])将为假,因为[0 => 'a']不在内部[0 => 'b', 1 => 'a']
Robert T.

对不起,但我必须同意罗伯特的意见。最初,我认为这是将数组与字符串键进行比较的好方法,但是assertEquals如果键的顺序不同,则已经可以解决这个问题。我刚刚测试过。
科多斯·约翰逊,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.