PHPUnit: afirmar que dos matrices son iguales, pero el orden de los elementos no es importante

132

¿Cuál es una buena manera de afirmar que dos conjuntos de objetos son iguales, cuando el orden de los elementos en el conjunto no es importante, o incluso está sujeto a cambios?

koen
fuente
¿Le preocupa que los objetos en la matriz sean iguales o solo que haya x cantidad de objeto y en ambas matrices?
Edorian
@edorian Ambos serían los más interesantes. En mi caso, aunque solo hay un objeto y en cada matriz.
koen
por favor defina igual . ¿Comparar hashes de objetos ordenados es lo que necesita? Probablemente tendrás que ordenar los objetos de todos modos.
tomas
@takeshin Igual que en ==. En mi caso, son objetos de valor, por lo que la igualdad no es necesaria. Probablemente podría crear un método de afirmación personalizado. Lo que necesitaría en él es contar el número de elementos en cada matriz, y para cada elemento en ambos en igual (==) debe existir.
koen
77
En realidad, en PHPUnit 3.7.24, $ this-> afirmarEquals afirma que la matriz contiene las mismas claves y valores, sin tener en cuenta en qué orden.
Dereckson

Respuestas:

38

La forma más limpia de hacer esto sería extender phpunit con un nuevo método de afirmación. Pero aquí hay una idea de una manera más simple por ahora. Código no probado, verifique:

En algún lugar de tu aplicación:

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

En tu prueba:

$this->assertTrue(arrays_are_similar($foo, $bar));
Craig
fuente
Craig, estás cerca de lo que intenté originalmente. En realidad, array_diff es lo que necesitaba, pero no parece funcionar para los objetos. Escribí
koen
El enlace correcto
Xavi Montero
cada parte es innecesaria: array_diff_assoc ya compara claves y valores. EDITAR: y debe verificar count(array_diff_assoc($b, $a))también.
JohnSmith
212

Puede usar el método ClaimEqualsCanonicalizing que se agregó en PHPUnit 7.5. Si compara las matrices utilizando este método, estas matrices se ordenarán por el comparador de matrices PHPUnit.

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

En versiones anteriores de PHPUnit, puede usar un parámetro no documentado $ canonicalize del método afirmarEquals . Si pasa $ canonicalize = true , obtendrá el mismo efecto:

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 fuente del comparador de matrices en la última versión de PHPUnit: https://github.com/sebastianbergmann/comparator/blob/master/src/ArrayComparator.php#L46

pryazhnikov
fuente
10
Fantástico. ¿Por qué esta no es la respuesta aceptada, @koen?
rinogo
77
Usar $delta = 0.0, $maxDepth = 10, $canonicalize = truepasar parámetros a la función es engañoso: PHP no admite argumentos con nombre. Lo que esto realmente está haciendo es establecer esas tres variables, luego pasar inmediatamente sus valores a la función. Esto causará problemas si esas tres variables ya están definidas en el ámbito local, ya que se sobrescribirán.
Yi Jiang
11
@ yi-jiang, es la forma más corta de explicar el significado de argumentos adicionales. Es más variante luego más limpia auto-descriptivo: $this->assertEquals($array1, $array2, "\$canonicalize = true", 0.0, 10, true);. Podría usar 4 líneas en lugar de 1, pero no hice eso.
pryazhnikov
8
No señala que esta solución descartará las claves.
Odalrick
8
tenga en cuenta que $canonicalizese eliminará: github.com/sebastianbergmann/phpunit/issues/3342 y assertEqualsCanonicalizing()lo reemplazará.
koen
35

Mi problema era que tenía 2 matrices (las claves de matriz no son relevantes para mí, solo los valores).

Por ejemplo, quería probar si

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

tenía el mismo contenido (orden no relevante para mí) como

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

Así que he usado array_diff .

El resultado final fue (si las matrices son iguales, la diferencia dará como resultado una matriz vacía). Tenga en cuenta que la diferencia se calcula en ambos sentidos (Gracias @beret, @GordonM)

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

Para un mensaje de error más detallado (durante la depuración), también puede probar de esta manera (gracias @ DenilsonSá):

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

Versión antigua con errores dentro:

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

Valentin Despa
fuente
El problema de este enfoque es que si $array1tiene más valores que $array2, entonces devuelve una matriz vacía aunque los valores de la matriz no sean iguales. También debe probar, ese tamaño de matriz es el mismo, para estar seguro.
petrkotek
3
Debes hacer array_diff o array_diff_assoc en ambos sentidos. Si una matriz es un superconjunto de la otra, entonces array_diff en una dirección estará vacía, pero no vacía en la otra. $a1 = [1,2,3,4,5]; $a2 = [1,3,5]; var_dump (array_diff ($a1, $a2)); var_dump (array_diff ($a2, $a1))
GordonM
2
assertEmptyno imprimirá la matriz si no está vacía, lo cual es inconveniente al depurar las pruebas. Sugeriría usar: $this->assertSame(array_diff($expected, $actual), array_diff($actual, $expected), $message);ya que esto imprimirá el mensaje de error más útil con el mínimo de código adicional. Esto funciona porque A \ B = B \ A ⇔ A \ B y B \ A están vacías ⇔ A = B
Denilson Sá Maia
Tenga en cuenta que array_diff convierte cada valor en cadena para comparar.
Konstantin Pelepelin
Para agregar a @checat: recibirá un Array to string conversionmensaje cuando intente convertir una matriz en una cadena. Una forma de evitar esto es usandoimplode
ub3rst4r
20

Otra posibilidad:

  1. Ordenar ambas matrices
  2. Conviértelos a una cadena
  3. Afirmar que ambas cadenas son iguales

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

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

$this->assertEquals(json_encode($exp), json_encode($arr));
rodrigo-silveira
fuente
Si alguna matriz contiene objetos, json_encode solo codifica las propiedades públicas. Esto seguirá funcionando, pero solo si todas las propiedades que determinan la igualdad son públicas. Eche un vistazo a la siguiente interfaz para controlar la codificación json de propiedades privadas. php.net/manual/en/class.jsonserializable.php
Westy92
1
Esto funciona incluso sin clasificar. Para assertEqualsel orden no importa.
Marchitez
1
De hecho, también podemos usar $this->assertSame($exp, $arr); una comparación similar, ya que la $this->assertEquals(json_encode($exp), json_encode($arr)); única diferencia es que no tenemos que usar json_encode
maxwells
15

Método de ayuda simple

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

O si necesita más información de depuración cuando las matrices no son iguales

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

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

Si la matriz es ordenable, los ordenaría a ambos antes de verificar la igualdad. Si no, los convertiría en conjuntos de algún tipo y los compararía.

Rodney Gitzel
fuente
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)));

O con 2 afirmaciones (más fácil de leer):

// 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
fuente
Eso es inteligente :)
Christian
Exactamente lo que estaba buscando. Sencillo.
Abdul Maye
6

Aunque no le importa el pedido, puede ser más fácil tenerlo en cuenta:

Tratar:

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

Utilizamos el siguiente método de envoltura en nuestras Pruebas:

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

Si las claves son las mismas pero fuera de servicio, esto debería resolverlo.

Solo tiene que obtener las claves en el mismo orden y comparar los 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
fuente
3

Las soluciones dadas no hicieron el trabajo por mí porque quería ser capaz de manejar una matriz multidimensional y tener un mensaje claro de lo que es diferente entre las dos matrices.

Aqui esta mi funcion

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

Entonces para usarlo

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

Escribí un código simple para obtener primero todas las claves de una 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;
}

Luego, para comprobar que estaban estructurados de la misma manera, independientemente del orden de las teclas:

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

HTH

Sturrockad
fuente
0

Si los valores son solo int o cadenas, y no hay matrices de niveles múltiples ...

¿Por qué no simplemente ordenar las matrices, convertirlas en cadenas ...

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

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

... y luego compara la cadena:

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

Si desea probar solo los valores de la matriz que puede hacer:

$this->assertEquals(array_values($arrayOne), array_values($arrayTwo));
Anderson Contreira
fuente
1
Desafortunadamente, eso no está probando "solo los valores" sino tanto los valores como el orden de los valores. Ej .echo("<pre>"); print_r(array_values(array("size" => "XL", "color" => "gold"))); print_r(array_values(array("color" => "gold", "size" => "XL")));
bolsillos y
-3

Otra opción, como si no tuviera ya suficiente, es combinar assertArraySubsetcombinado con assertCountpara hacer su afirmación. Entonces, su código se vería algo así.

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

De esta manera, usted es independiente del orden pero aún así afirma que todos sus elementos están presentes.

Jonathan
fuente
En assertArraySubsetel orden de los índices importa, por lo que no funcionará. es decir, self :: afirmarArraySubset (['a'], ['b', 'a']) será falso, porque [0 => 'a']no está adentro[0 => 'b', 1 => 'a']
Robert T.
Lo siento pero tengo que estar de acuerdo con Robert. Al principio pensé que esta sería una buena solución para comparar matrices con claves de cadena, pero assertEqualsya lo maneja si las claves no están en el mismo orden. Acabo de probarlo.
Kodos Johnson