PHPUnit: Assert zwei Arrays sind gleich, aber die Reihenfolge der Elemente ist nicht wichtig

132

Was ist ein guter Weg, um zu behaupten, dass zwei Arrays von Objekten gleich sind, wenn die Reihenfolge der Elemente im Array unwichtig ist oder sich sogar ändern kann?

koen
quelle
Interessiert es Sie, dass die Objekte im Array gleich sind oder nur, dass in beiden Arrays x Objektmenge y vorhanden ist?
Edorian
@edorian Beide wären am interessantesten. In meinem Fall gibt es jedoch nur ein Objekt y in jedem Array.
Koen
bitte gleich definieren . Vergleichen Sie sortierte Objekt-Hashes, was benötigen Sie? Sie müssen wahrscheinlich sowieso Objekte sortieren .
Takeshin
@takeshin Gleich wie in ==. In meinem Fall handelt es sich um Wertobjekte, sodass keine Gleichheit erforderlich ist. Ich könnte wahrscheinlich eine benutzerdefinierte Assert-Methode erstellen. Was ich darin brauchen würde, ist die Anzahl der Elemente in jedem Array zu zählen, und für jedes Element in beiden muss gleich (==) existieren.
Koen
7
In PHPUnit 3.7.24 bestätigt $ this-> assertEquals, dass das Array dieselben Schlüssel und Werte enthält, unabhängig von der Reihenfolge.
Dereckson

Antworten:

39

Der sauberste Weg, dies zu tun, wäre, phpunit mit einer neuen Assertionsmethode zu erweitern. Aber hier ist eine Idee für einen einfacheren Weg. Ungetesteter Code, bitte überprüfen Sie:

Irgendwo in Ihrer App:

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

In Ihrem Test:

$this->assertTrue(arrays_are_similar($foo, $bar));
Craig
quelle
Craig, du bist nah an dem, was ich ursprünglich versucht habe. Eigentlich brauchte ich array_diff, aber es scheint nicht für Objekte zu funktionieren. Ich habe meine benutzerdefinierte Behauptung wie hier erklärt geschrieben: phpunit.de/manual/current/en/extending-phpunit.html
koen
Der richtige Link ist jetzt mit https und ohne www: phpunit.de/manual/current/en/extending-phpunit.html
Xavi Montero
Jeder Teil ist nicht erforderlich - array_diff_assoc vergleicht bereits Schlüssel und Werte. EDIT: und Sie müssen auch überprüfen count(array_diff_assoc($b, $a)).
JohnSmith
212

Sie können die in PHPUnit 7.5 hinzugefügte assertEqualsCanonicalizing- Methode verwenden. Wenn Sie die Arrays mit dieser Methode vergleichen, werden diese Arrays nach dem PHPUnit-Arrays-Komparator selbst sortiert.

Codebeispiel:

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

In älteren Versionen von PHPUnit können Sie einen undokumentierten Parameter $ canonicalize der assertEquals- Methode verwenden. Wenn Sie $ canonicalize = true übergeben , erhalten Sie den gleichen Effekt:

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

Arrays-Komparator-Quellcode in der neuesten Version von PHPUnit: https://github.com/sebastianbergmann/comparator/blob/master/src/ArrayComparator.php#L46

pryazhnikov
quelle
10
Fantastisch. Warum ist dies nicht die akzeptierte Antwort, @koen?
Rinogo
7
Die $delta = 0.0, $maxDepth = 10, $canonicalize = trueÜbergabe von Parametern an die Funktion ist irreführend - PHP unterstützt keine benannten Argumente. Dabei werden diese drei Variablen festgelegt und ihre Werte sofort an die Funktion übergeben. Dies führt zu Problemen, wenn diese drei Variablen bereits im lokalen Bereich definiert sind, da sie überschrieben werden.
Yi Jiang
11
@ yi-jiang, es ist nur der kürzeste Weg, um die Bedeutung zusätzlicher Argumente zu erklären. Es ist selbstbeschreibender als eine sauberere Variante : $this->assertEquals($array1, $array2, "\$canonicalize = true", 0.0, 10, true);. Ich könnte 4 Zeilen anstelle von 1 verwenden, aber das habe ich nicht getan.
Pryazhnikov
8
Sie weisen nicht darauf hin, dass diese Lösung die Schlüssel verwirft.
Odalrick
8
Beachten Sie, $canonicalizedass entfernt wird: github.com/sebastianbergmann/phpunit/issues/3342 und assertEqualsCanonicalizing()ersetzt.
Koen
35

Mein Problem war, dass ich 2 Arrays hatte (Array-Schlüssel sind für mich nicht relevant, nur die Werte).

Zum Beispiel wollte ich testen, ob

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

hatte den gleichen Inhalt (Reihenfolge für mich nicht relevant) wie

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

Also habe ich array_diff verwendet .

Das Endergebnis war (wenn die Arrays gleich sind, führt die Differenz zu einem leeren Array). Bitte beachten Sie, dass der Unterschied in beide Richtungen berechnet wird (Danke @beret, @GordonM).

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

Für eine detailliertere Fehlermeldung (beim Debuggen) können Sie auch wie folgt testen (danke @ DenilsonSá):

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

Alte Version mit Fehlern im Inneren:

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

Valentin Despa
quelle
Das Problem dieses Ansatzes ist, dass wenn $array1mehr Werte als vorhanden sind $array2, ein leeres Array zurückgegeben wird, obwohl die Array-Werte nicht gleich sind. Sie sollten auch testen, ob die Arraygröße gleich ist, um sicherzugehen.
Petrkotek
3
Sie sollten array_diff oder array_diff_assoc in beide Richtungen ausführen. Wenn ein Array eine Obermenge des anderen ist, ist array_diff in einer Richtung leer, in der anderen jedoch nicht leer. $a1 = [1,2,3,4,5]; $a2 = [1,3,5]; var_dump (array_diff ($a1, $a2)); var_dump (array_diff ($a2, $a1))
GordonM
2
assertEmptydruckt das Array nicht, wenn es nicht leer ist, was beim Debuggen von Tests unpraktisch ist. Ich würde vorschlagen,: zu verwenden $this->assertSame(array_diff($expected, $actual), array_diff($actual, $expected), $message);, da dies die nützlichste Fehlermeldung mit einem Minimum an zusätzlichem Code druckt. Dies funktioniert, weil A \ B = B \ A ⇔ A \ B und B \ A leer sind ⇔ A = B
Denilson Sá Maia
Beachten Sie, dass array_diff jeden Wert zum Vergleich in einen String konvertiert.
Konstantin Pelepelin
So fügen Sie @checat hinzu: Sie erhalten eine Array to string conversionNachricht, wenn Sie versuchen, ein Array in eine Zeichenfolge umzuwandeln . Eine Möglichkeit, dies zu implode
umgehen,
20

Eine andere Möglichkeit:

  1. Sortieren Sie beide Arrays
  2. Konvertieren Sie sie in eine Zeichenfolge
  3. Stellen Sie sicher, dass beide Zeichenfolgen gleich sind

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

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

$this->assertEquals(json_encode($exp), json_encode($arr));
Rodrigo-Silveira
quelle
Wenn eines der Arrays Objekte enthält, codiert json_encode nur die öffentlichen Eigenschaften. Dies funktioniert weiterhin, jedoch nur, wenn alle Eigenschaften, die die Gleichheit bestimmen, öffentlich sind. Schauen Sie sich die folgende Oberfläche an, um die json_encoding von privaten Eigenschaften zu steuern. php.net/manual/en/class.jsonserializable.php
Westy92
1
Dies funktioniert auch ohne Sortierung. Für assertEqualsdie Bestellung spielt das keine Rolle.
Wilt
1
In der Tat können wir auch einen $this->assertSame($exp, $arr); ähnlichen Vergleich verwenden, da der $this->assertEquals(json_encode($exp), json_encode($arr)); einzige Unterschied darin besteht, dass wir json_encode
maxwells
15

Einfache Hilfsmethode

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

Oder wenn Sie mehr Debug-Informationen benötigen, wenn Arrays nicht gleich sind

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

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

Wenn das Array sortierbar ist, würde ich beide sortieren, bevor ich die Gleichheit überprüfe. Wenn nicht, würde ich sie in Sätze umwandeln und diese vergleichen.

Rodney Gitzel
quelle
6

Verwenden von 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)));

Oder mit 2 Behauptungen (leichter zu lesen):

// 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
quelle
Das ist klug :)
Christian
Genau das, wonach ich gesucht habe. Einfach.
Abdul Maye
6

Auch wenn Sie sich nicht für die Bestellung interessieren, ist es möglicherweise einfacher, dies zu berücksichtigen:

Versuchen:

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

In unseren Tests verwenden wir die folgende Wrapper-Methode:

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

Wenn die Schlüssel gleich, aber nicht in Ordnung sind, sollte dies das Problem lösen.

Sie müssen nur die Schlüssel in derselben Reihenfolge erhalten und die Ergebnisse vergleichen.

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

Die angegebenen Lösungen haben für mich nicht funktioniert, weil ich in der Lage sein wollte, mehrdimensionale Arrays zu verarbeiten und eine klare Botschaft darüber zu erhalten, was zwischen den beiden Arrays unterschiedlich ist.

Hier ist meine Funktion

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

Dann, um es zu benutzen

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

Ich habe einen einfachen Code geschrieben, um zuerst alle Schlüssel aus einem mehrdimensionalen Array zu erhalten:

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

Um dann zu testen, ob sie unabhängig von der Reihenfolge der Schlüssel gleich aufgebaut sind:

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

HTH

sturrockad
quelle
0

Wenn die Werte nur int oder Strings sind und keine Arrays mit mehreren Ebenen ....

Warum nicht einfach die Arrays sortieren, sie in einen String konvertieren ...

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

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

... und dann String vergleichen:

    $this->assertEquals($myExpectedArray, $myArray);
Koalaok
quelle
-2

Wenn Sie nur die Werte des Arrays testen möchten, können Sie Folgendes tun:

$this->assertEquals(array_values($arrayOne), array_values($arrayTwo));
Anderson Contreira
quelle
1
Leider testet das nicht "nur die Werte", sondern sowohl die Werte als auch die Reihenfolge der Werte. ZBecho("<pre>"); print_r(array_values(array("size" => "XL", "color" => "gold"))); print_r(array_values(array("color" => "gold", "size" => "XL")));
Pocketsand
-3

Eine andere Möglichkeit, als ob Sie noch nicht genug hätten, ist die assertArraySubsetKombination mit assertCount, um Ihre Behauptung aufzustellen. Ihr Code würde also ungefähr so ​​aussehen.

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

Auf diese Weise sind Sie auftragsunabhängig und behaupten dennoch, dass alle Ihre Elemente vorhanden sind.

Jonathan
quelle
In assertArraySubsetder Reihenfolge der Indizes ist es wichtig, dass es nicht funktioniert. dh self :: assertArraySubset (['a'], ['b', 'a']) wird falsch sein, weil [0 => 'a']es nicht drinnen ist[0 => 'b', 1 => 'a']
Robert T.
Entschuldigung, aber ich muss Robert zustimmen. Zuerst dachte ich, dass dies eine gute Lösung wäre, um Arrays mit String-Schlüsseln zu vergleichen, aber das wird assertEqualsbereits erledigt, wenn die Schlüssel nicht in derselben Reihenfolge sind. Ich habe es gerade getestet.
Kodos Johnson