PHPUnit: afirma que duas matrizes são iguais, mas a ordem dos elementos não é importante

132

Qual é uma boa maneira de afirmar que duas matrizes de objetos são iguais, quando a ordem dos elementos na matriz não é importante ou está sujeita a alterações?

koen
fonte
Você se importa com os objetos na matriz serem iguais ou apenas com a quantidade x de objeto y nas duas matrizes?
Edorian 1/10/10
@edorian Ambos seriam os mais interessantes. No meu caso, embora haja apenas um objeto y em cada matriz.
Koen
por favor defina igual . É para comparar hashes de objetos classificados o que você precisa? Você provavelmente terá que classificar objetos de qualquer maneira.
takeshin
@takeshin Igual como em ==. No meu caso, são objetos de valor, portanto, a mesmice não é necessária. Eu provavelmente poderia criar um método de declaração personalizado. O que eu precisaria é contar o número de elementos em cada matriz, e para cada elemento em ambos iguais (==) deve existir.
koen
7
Na verdade, no PHPUnit 3.7.24, $ this-> assertEquals afirma que a matriz contém as mesmas chaves e valores, desconsiderando em que ordem.
Dereckson

Respostas:

38

A maneira mais limpa de fazer isso seria estender o phpunit com um novo método de asserção. Mas aqui está uma idéia para uma maneira mais simples por enquanto. Código não testado, verifique:

Em algum lugar do seu aplicativo:

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

No seu teste:

$this->assertTrue(arrays_are_similar($foo, $bar));
Craig
fonte
Craig, você está perto do que tentei originalmente. Na verdade, array_diff é o que eu precisava, mas parece não funcionar para objetos. Eu escrevi minha asserção personalizada, conforme explicado aqui: phpunit.de/manual/current/en/extending-phpunit.html
koen
Ligação adequada agora é com https e sem www: phpunit.de/manual/current/en/extending-phpunit.html
Xavi Montero
cada parte é desnecessária - array_diff_assoc já compara chaves e valores. EDIT: e você precisa verificar count(array_diff_assoc($b, $a))também.
19418 JohnSmith
212

Você pode usar o método assertEqualsCanonicalizing que foi adicionado no PHPUnit 7.5. Se você comparar as matrizes usando esse método, essas matrizes serão classificadas pelo próprio comparador de matrizes PHPUnit.

Exemplo de código:

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

Nas versões mais antigas do PHPUnit, você pode usar um parâmetro não documentado, $ canonicalize of assertEquals . Se você passar $ canonicalize = true , obterá o mesmo efeito:

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

Código-fonte do comparador de matrizes na versão mais recente do PHPUnit: https://github.com/sebastianbergmann/comparator/blob/master/src/ArrayComparator.php#L46

pryazhnikov
fonte
10
Fantástico. Por que essa não é a resposta aceita, @koen?
Rinogo 31/07/2015
7
Usar $delta = 0.0, $maxDepth = 10, $canonicalize = truepara passar parâmetros para a função é enganoso - o PHP não suporta argumentos nomeados. O que isso realmente está fazendo é definir essas três variáveis ​​e passar imediatamente seus valores para a função. Isso causará problemas se essas três variáveis ​​já estiverem definidas no escopo local, pois serão substituídas.
Yi Jiang
11
@ yi-jiang, é apenas a maneira mais curta de explicar o significado de argumentos adicionais. É mais variante, em seguida, mais limpo auto-descritivo: $this->assertEquals($array1, $array2, "\$canonicalize = true", 0.0, 10, true);. Eu poderia usar 4 linhas em vez de 1, mas não fiz isso.
Pryazhnikov 15/10/2015
8
Você não aponta que esta solução descartará as chaves.
Odalrick 26/05
8
observe que $canonicalizeserá removido: github.com/sebastianbergmann/phpunit/issues/3342 e assertEqualsCanonicalizing()o substituirá.
quer
35

Meu problema era que eu tinha 2 matrizes (chaves de matriz não são relevantes para mim, apenas os valores).

Por exemplo, eu queria testar se

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

tinha o mesmo conteúdo (pedido não relevante para mim) que

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

Então, eu usei array_diff .

O resultado final foi (se as matrizes forem iguais, a diferença resultará em uma matriz vazia). Observe que a diferença é calculada nos dois sentidos (Obrigado @beret, @GordonM)

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

Para uma mensagem de erro mais detalhada (durante a depuração), você também pode testar desta forma (obrigado @ DenilsonSá):

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

Versão antiga com erros dentro:

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

Valentin Despa
fonte
O problema dessa abordagem é que, se $array1tiver mais valores que $array2, retornará uma matriz vazia, mesmo que os valores da matriz não sejam iguais. Você também deve testar se o tamanho da matriz é o mesmo, para ter certeza.
Petrkotek
3
Você deve fazer o array_diff ou array_diff_assoc nos dois sentidos. Se uma matriz for um superconjunto da outra, o array_diff em uma direção estará vazio, mas não vazio na outra. $a1 = [1,2,3,4,5]; $a2 = [1,3,5]; var_dump (array_diff ($a1, $a2)); var_dump (array_diff ($a2, $a1))
GordonM
2
assertEmptynão imprimirá a matriz se não estiver vazia, o que é inconveniente durante a depuração de testes. Eu sugiro usar:, $this->assertSame(array_diff($expected, $actual), array_diff($actual, $expected), $message);pois isso imprimirá a mensagem de erro mais útil com o mínimo de código extra. Isto funciona porque A \ B = B \ Uma ⇔ A \ B e B \ A são vazio ⇔ A = B
Denilson Sá Maia
Observe que array_diff converte todos os valores em string para comparação.
9609 Konstantin Pelepelin
Para adicionar ao @checat: você receberá uma Array to string conversionmensagem ao tentar converter uma matriz em uma string. Uma maneira de contornar isso é usandoimplode
ub3rst4r
20

Outra possibilidade:

  1. Classificar as duas matrizes
  2. Converta-os em uma string
  3. Afirme que ambas as cadeias são iguais

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

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

$this->assertEquals(json_encode($exp), json_encode($arr));
rodrigo-silveira
fonte
Se uma das matrizes contiver objetos, json_encode codifica apenas as propriedades públicas. Isso ainda funcionará, mas apenas se todas as propriedades que determinam a igualdade forem públicas. Dê uma olhada na interface a seguir para controlar json_encoding de propriedades particulares. php.net/manual/en/class.jsonserializable.php
Westy92 23/09/15
1
Isso funciona mesmo sem classificação. Para assertEqualso pedido não importa.
Wilt
1
Na verdade, também podemos usar $this->assertSame($exp, $arr); o que faz comparação semelhante como $this->assertEquals(json_encode($exp), json_encode($arr)); única diferença é que não tem que usar json_encode
maxwells
15

Método auxiliar simples

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

Ou se você precisar de mais informações de depuração quando matrizes não forem iguais

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

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

Se a matriz for classificável, eu os classificaria antes de verificar a igualdade. Caso contrário, eu os converteria em conjuntos de algum tipo e os compararia.

Rodney Gitzel
fonte
6

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

Ou com 2 afirmações (mais fáceis de ler):

// 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)));
caligari
fonte
Isso é :) inteligente
Christian
Exatamente o que eu estava procurando. Simples.
precisa
6

Mesmo que você não se importe com o pedido, pode ser mais fácil levar isso em consideração:

Experimentar:

asort($foo);
asort($bar);
$this->assertEquals($foo, $bar);
Antonis Charalambous
fonte
5

Usamos o seguinte método de invólucro em nossos testes:

/**
 * 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
fonte
5

Se as chaves forem as mesmas, mas fora de ordem, isso deve resolver o problema.

Você só precisa obter as chaves na mesma ordem e comparar os resultados.

 /**
 * 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
fonte
3

As soluções fornecidas não funcionaram para mim porque eu queria poder lidar com uma matriz multidimensional e ter uma mensagem clara do que é diferente entre as duas matrizes.

Aqui está a minha função

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

Então para usá-lo

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

Eu escrevi um código simples para primeiro obter todas as chaves de uma matriz multidimensional:

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

Depois, para testar se eles foram estruturados da mesma maneira, independentemente da ordem das chaves:

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

HTH

sturrockad
fonte
0

Se os valores são apenas int ou strings, e nenhuma matriz de vários níveis ....

Por que não apenas classificar as matrizes, convertê-las em string ...

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

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

... e depois compare a string:

    $this->assertEquals($myExpectedArray, $myArray);
koalaok
fonte
-2

Se você deseja testar apenas os valores da matriz, pode fazer:

$this->assertEquals(array_values($arrayOne), array_values($arrayTwo));
Anderson Contreira
fonte
1
Infelizmente, isso não está testando "apenas os valores", mas os valores e a ordem dos valores. Por exemploecho("<pre>"); print_r(array_values(array("size" => "XL", "color" => "gold"))); print_r(array_values(array("color" => "gold", "size" => "XL")));
Pocketsand
-3

Outra opção, como se você já não tivesse o suficiente, é combinar assertArraySubsetcombinado com assertCountpara fazer sua afirmação. Portanto, seu código seria algo parecido.

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

Dessa forma, você é independente da ordem, mas ainda afirma que todos os seus elementos estão presentes.

Jonathan
fonte
Na assertArraySubsetordem dos índices, importa para que não funcione. ou seja self :: assertArraySubset ([ 'a'], [ 'b', 'a']) será falso, porque [0 => 'a']não está dentro[0 => 'b', 1 => 'a']
Robert T.
Desculpe, mas eu tenho que concordar com Robert. No começo, pensei que essa seria uma boa solução para comparar matrizes com chaves de string, mas assertEqualsjá lida com isso se as chaves não estiverem na mesma ordem. Eu apenas testei.
Kodo Johnson