PHPUnit: утверждают, что два массива равны, но порядок элементов не важен

132

Как правильно утверждать, что два массива объектов равны, когда порядок элементов в массиве не важен или даже может быть изменен?

Koen
источник
Вы заботитесь о том, чтобы объекты в массиве были равными, или просто о том, что в обоих массивах есть x объектов y?
edorian 01
@edorian И то, и другое было бы очень интересно. В моем случае в каждом массиве есть только один объект y.
коэн
укажите равные . Вам нужно сравнение отсортированных хэшей объектов ? Возможно, вам все равно придется отсортировать объекты .
Takehin 01
@takeshin Равно как в ==. В моем случае они являются объектами-ценностями, поэтому в единообразии нет необходимости. Вероятно, я мог бы создать собственный метод утверждения. Что мне нужно в нем, так это подсчитать количество элементов в каждом массиве, и для каждого элемента в обоих должно существовать равное (==).
Коэн
7
Фактически, в PHPUnit 3.7.24 $ this-> assertEquals утверждает, что массив содержит те же ключи и значения, независимо от того, в каком порядке.
Dereckson

Ответы:

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/exnding-phpunit.html
Хави Монтеро,
Часть 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, но я этого не делал.
Пряжников
8
Вы не указываете, что это решение отбросит ключи.
Odalrick
8
обратите внимание, что $canonicalizeбудет удалено: github.com/sebastianbergmann/phpunit/issues/3342 и assertEqualsCanonicalizing()заменит его.
коэн
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
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 преобразует каждое значение в строку для сравнения.
Константин Пелепелин
Чтобы добавить в @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));
Rodrigo-Силвейра
источник
Если какой-либо массив содержит объекты, json_encode кодирует только общедоступные свойства. Это по-прежнему будет работать, но только если все свойства, определяющие равенство, являются общедоступными. Взгляните на следующий интерфейс для управления json_encoding частных свойств. php.net/manual/en/class.jsonserializable.php
Westy92
1
Это работает даже без сортировки. Для assertEqualsпорядка значения не имеет.
Уилт
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);
}
ksimka
источник
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)));

Или с двумя утверждениями (легче читать):

// 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)));
Калигари
источник
Это умно :)
Кристиан
Именно то, что я искал. Просто.
Абдул Майе
6

Даже если вас не волнует порядок, может быть проще это учесть:

Пытаться:

asort($foo);
asort($bar);
$this->assertEquals($foo, $bar);
Антонис Хараламбус
источник
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);
}
t.heintz
источник
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);
}
Cris
источник
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("/"));
moins52
источник
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));

НТН

sturrockad
источник
0

Если значения представляют собой просто int или строки, а не многоуровневые массивы ....

Почему бы просто не отсортировать массивы, преобразовать их в строку ...

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

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

... а затем сравните строку:

    $this->assertEquals($myExpectedArray, $myArray);
koalaok
источник
-2

Если вы хотите проверить только значения массива, вы можете:

$this->assertEquals(array_values($arrayOne), array_values($arrayTwo));
Андерсон Контрейра
источник
1
К сожалению, это не проверка «только значений», но и значений, и порядка значений. Egecho("<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']
Роберт Т.
Извините, но я должен согласиться с Робертом. Сначала я подумал, что это было бы хорошим решением для сравнения массивов со строковыми ключами, но assertEqualsуже обрабатывает это, если ключи не в том же порядке. Я только что это протестировал.
Кодос Джонсон