PHPUnit: affirme que deux tableaux sont égaux, mais l'ordre des éléments n'est pas important

132

Quel est un bon moyen d'affirmer que deux tableaux d'objets sont égaux, lorsque l'ordre des éléments dans le tableau est sans importance, ou même sujet à changement?

Koen
la source
Vous souciez-vous que les objets du tableau soient égaux ou simplement qu'il y ait x quantité d'objets y dans les deux tableaux?
édorien
@edorian Les deux seraient des plus intéressants. Dans mon cas, il n'y a qu'un seul objet y dans chaque tableau.
koen
veuillez définir égal . La comparaison des hachages d'objets triés est-elle ce dont vous avez besoin? Vous devrez probablement trier les objets de toute façon.
1er
@takeshin Equal comme dans ==. Dans mon cas, ce sont des objets de valeur, donc la similitude n'est pas nécessaire. Je pourrais probablement créer une méthode d'assert personnalisée. Ce dont j'aurais besoin, c'est de compter le nombre d'éléments dans chaque tableau, et pour chaque élément des deux à égalité (==) doit exister.
koen
7
En fait, sur PHPUnit 3.7.24, $ this-> assertEquals affirme que le tableau contient les mêmes clés et valeurs, sans tenir compte de l'ordre.
Dereckson

Réponses:

38

La façon la plus propre de faire cela serait d'étendre phpunit avec une nouvelle méthode d'assertion. Mais voici une idée pour un moyen plus simple pour le moment. Code non testé, veuillez vérifier:

Quelque part dans votre application:

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

Dans votre test:

$this->assertTrue(arrays_are_similar($foo, $bar));
Craig
la source
Craig, tu es proche de ce que j'ai essayé au départ. En fait, array_diff est ce dont j'avais besoin, mais cela ne semble pas fonctionner pour les objets. J'ai écrit mon assertion personnalisée comme expliqué ici: phpunit.de/manual/current/en/extending-phpunit.html
koen
Le lien approprié est maintenant avec https et sans www: phpunit.de/manual/current/en/extending-phpunit.html
Xavi Montero
foreach est inutile - array_diff_assoc compare déjà les clés et les valeurs. EDIT: et vous devez count(array_diff_assoc($b, $a))également vérifier .
JohnSmith
212

Vous pouvez utiliser la méthode assertEqualsCanonicalizing qui a été ajoutée dans PHPUnit 7.5. Si vous comparez les tableaux en utilisant cette méthode, ces tableaux seront triés par le comparateur de tableaux PHPUnit lui-même.

Exemple de code:

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

Dans les anciennes versions de PHPUnit, vous pouvez utiliser un paramètre non documenté $ canonicalize de la méthode assertEquals . Si vous passez $ canonicalize = true , vous obtiendrez le même effet:

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

Code source du comparateur de tableaux à la dernière version de PHPUnit: https://github.com/sebastianbergmann/comparator/blob/master/src/ArrayComparator.php#L46

pryazhnikov
la source
10
Fantastique. Pourquoi n'est-ce pas la réponse acceptée, @koen?
rinogo
7
Utiliser $delta = 0.0, $maxDepth = 10, $canonicalize = truepour passer des paramètres dans la fonction est trompeur - PHP ne prend pas en charge les arguments nommés. Ce que cela fait en fait est de définir ces trois variables, puis de transmettre immédiatement leurs valeurs à la fonction. Cela posera des problèmes si ces trois variables sont déjà définies dans la portée locale puisqu'elles seront écrasées.
Yi Jiang
11
@ yi-jiang, c'est juste le moyen le plus court d'expliquer la signification d'arguments supplémentaires. Il est plus variante auto-descriptif alors plus propre: $this->assertEquals($array1, $array2, "\$canonicalize = true", 0.0, 10, true);. Je pourrais utiliser 4 lignes au lieu de 1, mais je ne l'ai pas fait.
pryazhnikov
8
Vous ne faites pas remarquer que cette solution supprimera les clés.
Odalrick
8
notez que $canonicalizecela sera supprimé: github.com/sebastianbergmann/phpunit/issues/3342 et assertEqualsCanonicalizing()le remplacera.
koen
35

Mon problème était que j'avais 2 tableaux (les clés de tableau ne sont pas pertinentes pour moi, juste les valeurs).

Par exemple, je voulais tester si

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

avait le même contenu (ordre non pertinent pour moi) que

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

J'ai donc utilisé array_diff .

Le résultat final était (si les tableaux sont égaux, la différence se traduira par un tableau vide). Veuillez noter que la différence est calculée dans les deux sens (Merci @beret, @GordonM)

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

Pour un message d'erreur plus détaillé (lors du débogage), vous pouvez également tester comme ceci (merci @ DenilsonSá):

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

Ancienne version avec des bugs à l'intérieur:

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

Valentin Despa
la source
Le problème de cette approche est que si $array1a plus de valeurs que $array2, alors il renvoie un tableau vide même si les valeurs du tableau ne sont pas égales. Vous devriez également tester, que la taille du tableau est la même, pour être sûr.
petrkotek
3
Vous devez faire le array_diff ou array_diff_assoc dans les deux sens. Si un tableau est un sur-ensemble de l'autre, array_diff dans une direction sera vide, mais non vide dans l'autre. $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'imprimera pas le tableau s'il n'est pas vide, ce qui n'est pas pratique lors du débogage des tests. Je suggère d'utiliser:, $this->assertSame(array_diff($expected, $actual), array_diff($actual, $expected), $message);car cela imprimera le message d'erreur le plus utile avec le minimum de code supplémentaire. Cela fonctionne parce que A \ B = B \ A ⇔ A \ B et B \ A sont vides ⇔ A = B
Denilson Sá Maia
Notez que array_diff convertit chaque valeur en chaîne à des fins de comparaison.
Konstantin Pelepelin
Pour ajouter à @checat: vous recevrez un Array to string conversionmessage lorsque vous tenterez de convertir un tableau en chaîne. Un moyen de contourner ce implode
problème
20

Une autre possibilité:

  1. Trier les deux tableaux
  2. Convertissez-les en chaîne
  3. Affirmez que les deux chaînes sont égales

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

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

$this->assertEquals(json_encode($exp), json_encode($arr));
rodrigo-silveira
la source
Si l'un des tableaux contient des objets, json_encode encode uniquement les propriétés publiques. Cela fonctionnera toujours, mais uniquement si toutes les propriétés qui déterminent l'égalité sont publiques. Jetez un œil à l'interface suivante pour contrôler json_encoding des propriétés privées. php.net/manual/en/class.jsonserializable.php
Westy92
1
Cela fonctionne même sans tri. Car assertEqualsl'ordre n'a pas d'importance.
Flétrissement du
1
En effet, on peut aussi utiliser $this->assertSame($exp, $arr); qui ne comparaison similaire que $this->assertEquals(json_encode($exp), json_encode($arr)); seule différence est que nous ne devons pas utiliser json_encode
maxwells
15

Méthode d'aide simple

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

Ou si vous avez besoin de plus d'informations de débogage lorsque les tableaux ne sont pas égaux

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

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

Si le tableau est triable, je les trierais tous les deux avant de vérifier l'égalité. Sinon, je les convertirais en ensembles d'une sorte et les comparerais.

Rodney Gitzel
la source
6

En utilisant 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 avec 2 assertions (plus faciles à lire):

// 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
la source
C'est intelligent :)
Christian
Exactement ce que je cherchais. Facile.
Abdul Maye
6

Même si vous ne vous souciez pas de la commande, il peut être plus facile d'en tenir compte:

Essayer:

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

Nous utilisons la méthode wrapper suivante dans nos tests:

/**
 * 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
la source
5

Si les clés sont les mêmes mais dans le désordre, cela devrait le résoudre.

Il vous suffit de récupérer les clés dans le même ordre et de comparer les résultats.

 /**
 * 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
la source
3

Les solutions données n'ont pas fait le travail pour moi parce que je voulais être capable de gérer des tableaux multidimensionnels et d'avoir un message clair sur ce qui est différent entre les deux tableaux.

Voici ma fonction

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

Alors pour l'utiliser

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

J'ai écrit un code simple pour obtenir d'abord toutes les clés d'un tableau multidimensionnel:

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

Ensuite pour tester qu'ils étaient structurés de la même manière quel que soit l'ordre des clés:

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

HTH

Sturrockad
la source
0

Si les valeurs ne sont que des int ou des chaînes, et pas de tableaux à plusieurs niveaux ...

Pourquoi ne pas simplement trier les tableaux, les convertir en chaîne ...

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

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

... puis comparez la chaîne:

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

Si vous voulez tester uniquement les valeurs du tableau, vous pouvez faire:

$this->assertEquals(array_values($arrayOne), array_values($arrayTwo));
Anderson Contreira
la source
1
Malheureusement, cela ne teste pas «seulement les valeurs», mais à la fois les valeurs et l'ordre des valeurs. Par exempleecho("<pre>"); print_r(array_values(array("size" => "XL", "color" => "gold"))); print_r(array_values(array("color" => "gold", "size" => "XL")));
Pocketsand
-3

Une autre option, comme si vous n'en aviez pas déjà assez, est de combiner assertArraySubsetavec assertCountpour faire votre affirmation. Donc, votre code ressemblerait à quelque chose comme.

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

De cette façon, vous êtes indépendant de l'ordre tout en affirmant que tous vos éléments sont présents.

Jonathan
la source
Dans assertArraySubsetl'ordre des index, cela ne fonctionnera pas. ie self :: assertArraySubset (['a'], ['b', 'a']) sera faux, car il [0 => 'a']n'est pas à l'intérieur[0 => 'b', 1 => 'a']
Robert T.
Désolé mais je dois être d'accord avec Robert. Au début, je pensais que ce serait une bonne solution pour comparer des tableaux avec des clés de chaîne, mais je assertEqualsgère déjà cela si les clés ne sont pas dans le même ordre. Je viens de le tester.
Kodos Johnson du