PHPUnit: upewnij się, że dwie tablice są równe, ale kolejność elementów nie jest ważna

136

Jaki jest dobry sposób na stwierdzenie, że dwie tablice obiektów są równe, gdy kolejność elementów w tablicy jest nieistotna lub nawet podlega zmianie?

koen
źródło
Czy zależy Ci na tym, aby obiekty w tablicy były równe, czy po prostu na tym, że istnieje x ilość obiektu y w obu tablicach?
edorian
@edorian Oba byłyby najbardziej interesujące. W moim przypadku jednak w każdej tablicy jest tylko jeden obiekt y.
koen
proszę zdefiniować równe . Czy potrzebujesz porównania posortowanych skrótów obiektów ? Prawdopodobnie i tak będziesz musiał posortować obiekty .
takeshin
@takeshin Równe jak w ==. W moim przypadku są to przedmioty wartościowe, więc identyczność nie jest konieczna. Prawdopodobnie mógłbym stworzyć niestandardową metodę potwierdzania. To, czego potrzebowałbym w tym, to policzyć liczbę elementów w każdej tablicy, a dla każdego elementu w obu na równych (==) musi istnieć.
koen
7
Właściwie w PHPUnit 3.7.24 $ this-> assertEquals potwierdza, że ​​tablica zawiera te same klucze i wartości, bez względu na kolejność.
Dereckson,

Odpowiedzi:

40

Najprostszym sposobem na to byłoby rozszerzenie phpunita o nową metodę asercji. Ale teraz mam pomysł na prostszy sposób. Niesprawdzony kod, zweryfikuj:

Gdzieś w Twojej aplikacji:

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

W twoim teście:

$this->assertTrue(arrays_are_similar($foo, $bar));
Craig
źródło
Craig, jesteś blisko tego, czego początkowo próbowałem. Właściwie to array_diff jest tym, czego potrzebowałem, ale wydaje się, że nie działa dla obiektów. Napisałem swoje niestandardowe potwierdzenie, jak wyjaśniono tutaj: phpunit.de/manual/current/en/extending-phpunit.html
koen
Właściwy link jest teraz z https i bez www: phpunit.de/manual/current/en/extending-phpunit.html
Xavi Montero
każda część jest niepotrzebna - array_diff_assoc już porównuje zarówno klucze, jak i wartości. EDYCJA: i musisz count(array_diff_assoc($b, $a))też sprawdzić .
JohnSmith,
223

Możesz użyć metody assertEqualsCanonicalizing , która została dodana w PHPUnit 7.5. Jeśli porównasz tablice za pomocą tej metody, te tablice zostaną posortowane przez sam komparator tablic PHPUnit.

Przykład kodu:

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

W starszych wersjach PHPUnit można użyć nieudokumentowanego parametru $ canonicalize metody assertEquals . Jeśli przekażesz $ canonicalize = true , uzyskasz ten sam efekt:

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

Kod źródłowy komparatora tablic w najnowszej wersji PHPUnit: https://github.com/sebastianbergmann/comparator/blob/master/src/ArrayComparator.php#L46

pryazhnikov
źródło
10
Fantastyczny. Dlaczego to nie jest akceptowana odpowiedź, @koen?
rinogo
7
Używanie $delta = 0.0, $maxDepth = 10, $canonicalize = truedo przekazywania parametrów do funkcji jest mylące - PHP nie obsługuje nazwanych argumentów. W rzeczywistości ustawia te trzy zmienne, a następnie natychmiast przekazuje ich wartości do funkcji. Spowoduje to problemy, jeśli te trzy zmienne są już zdefiniowane w zakresie lokalnym, ponieważ zostaną nadpisane.
Yi Jiang,
11
@ yi-jiang, to najkrótszy sposób na wyjaśnienie znaczenia dodatkowych argumentów. To bardziej self-opisowy wtedy bardziej czyste wariant: $this->assertEquals($array1, $array2, "\$canonicalize = true", 0.0, 10, true);. Mogłem użyć 4 linii zamiast 1, ale tego nie zrobiłem.
pryazhnikov
9
Nie zwracasz uwagi, że to rozwiązanie spowoduje odrzucenie kluczy.
Odalrick
8
zauważ, że $canonicalizezostanie usunięty: github.com/sebastianbergmann/phpunit/issues/3342 i assertEqualsCanonicalizing()zastąpi go.
koen
35

Mój problem polegał na tym, że miałem 2 tablice (klucze tablic nie są dla mnie istotne, tylko wartości).

Na przykład chciałem sprawdzić, czy

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

miał taką samą treść (zamówienie nie jest dla mnie istotne) co

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

Więc użyłem array_diff .

Ostateczny wynik to (jeśli tablice są równe, różnica spowoduje pustą tablicę). Należy pamiętać, że różnica jest obliczana w obie strony (dzięki @beret, @GordonM)

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

Aby uzyskać bardziej szczegółowy komunikat o błędzie (podczas debugowania), możesz również przetestować w ten sposób (dzięki @ DenilsonSá):

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

Stara wersja z błędami w środku:

$ this-> assertEmpty (array_diff ($ tablica2, $ tablica1));

Valentin Despa
źródło
Problemem tego podejścia jest to, że jeśli $array1ma więcej wartości niż $array2, zwraca pustą tablicę, mimo że wartości tablicy nie są równe. Dla pewności należy również sprawdzić, czy rozmiar tablicy jest taki sam.
petrkotek
3
Należy wykonać operację array_diff lub array_diff_assoc w obie strony. Jeśli jedna tablica jest nadzbiorem drugiej, to array_diff w jednym kierunku będzie pusta, ale niepusta w drugim. $a1 = [1,2,3,4,5]; $a2 = [1,3,5]; var_dump (array_diff ($a1, $a2)); var_dump (array_diff ($a2, $a1))
GordonM
2
assertEmptynie wypisze tablicy, jeśli nie jest pusta, co jest niewygodne podczas debugowania testów. Sugerowałbym użycie:, $this->assertSame(array_diff($expected, $actual), array_diff($actual, $expected), $message);ponieważ spowoduje to wydrukowanie najbardziej użytecznego komunikatu o błędzie z minimalną ilością dodatkowego kodu. To działa, ponieważ A \ B = B \ A ⇔ A \ B i B \ A są puste ⇔ A = B
Denilson Sá Maia
Zauważ, że array_diff konwertuje każdą wartość na łańcuch dla porównania.
Konstantin Pelepelin,
Aby dodać do @checat: otrzymasz Array to string conversionkomunikat, gdy spróbujesz rzutować tablicę na łańcuch. Sposobem na obejście tego jest użycieimplode
ub3rst4r
22

Jeszcze jedna możliwość:

  1. Sortuj obie tablice
  2. Zamień je na ciąg
  3. Upewnij się, że oba ciągi są równe

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

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

$this->assertEquals(json_encode($exp), json_encode($arr));
rodrigo-silveira
źródło
Jeśli jedna z tablic zawiera obiekty, json_encode koduje tylko właściwości publiczne. Będzie to nadal działać, ale tylko wtedy, gdy wszystkie właściwości określające równość są publiczne. Spójrz na następujący interfejs, aby kontrolować json_encoding prywatnych właściwości. php.net/manual/en/class.jsonserializable.php
Westy92
1
Działa to nawet bez sortowania. Dla assertEqualsporządku nie ma znaczenia.
Wilt
1
Rzeczywiście, możemy również użyć tego, $this->assertSame($exp, $arr); co robi podobne porównanie, ponieważ $this->assertEquals(json_encode($exp), json_encode($arr)); jedyną różnicą jest to, że nie musimy używać json_encode
maxwells
15

Prosta metoda pomocnicza

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

Lub jeśli potrzebujesz więcej informacji do debugowania, gdy tablice nie są równe

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

    $this->assertEquals($expected, $actual, $message);
}
ksimka
źródło
8

Jeśli tablica jest sortowalna, posortowałbym je obie przed sprawdzeniem równości. Jeśli nie, zamieniłbym je na jakieś zestawy i porównałbym je.

Rodney Gitzel
źródło
6

Korzystanie z 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)));

Lub z 2 potwierdzeniami (łatwiejsze do odczytania):

// 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
źródło
To mądre :)
Christian
Dokładnie to, czego szukałem. Prosty.
Abdul Maye
6

Nawet jeśli nie zależy Ci na zamówieniu, może być łatwiej wziąć to pod uwagę:

Próbować:

asort($foo);
asort($bar);
$this->assertEquals($foo, $bar);
Antonis Charalambous
źródło
5

W naszych testach używamy następującej metody pakowania:

/**
 * 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
źródło
5

Jeśli klucze są takie same, ale nie działają, powinno to rozwiązać problem.

Musisz tylko zdobyć klucze w tej samej kolejności i porównać wyniki.

 /**
 * 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);
}
Kryzys
źródło
3

Podane rozwiązania nie sprawdziły się za mnie, ponieważ chciałem być w stanie obsłużyć wielowymiarowe tablice i mieć jasny komunikat o tym, co różni się między dwiema tablicami.

Oto moja funkcja

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

Potem go użyć

$this->assertArrayEquals($array1, $array2, array("/"));
moins52
źródło
1

Napisałem prosty kod, aby najpierw uzyskać wszystkie klucze z wielowymiarowej tablicy:

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

Następnie, aby sprawdzić, czy mają taką samą strukturę niezależnie od kolejności kluczy:

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

HTH

Sturrockad
źródło
-1

Jeśli wartości to tylko int lub stringi i nie ma tablic wielopoziomowych ....

Dlaczego nie posortować tablice, przekonwertować je na łańcuchy ...

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

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

... a następnie porównaj ciąg:

    $this->assertEquals($myExpectedArray, $myArray);
koalaok
źródło
-2

Jeśli chcesz przetestować tylko wartości tablicy, możesz to zrobić:

$this->assertEquals(array_values($arrayOne), array_values($arrayTwo));
Anderson Contreira
źródło
1
Niestety nie chodzi o testowanie „tylko wartości”, ale zarówno wartości, jak i kolejności wartości. Np.echo("<pre>"); print_r(array_values(array("size" => "XL", "color" => "gold"))); print_r(array_values(array("color" => "gold", "size" => "XL")));
Kieszenie i
-3

Inną opcją, jakbyś nie miał jeszcze wystarczająco dużo, jest połączenie w assertArraySubsetpołączeniu z, assertCountaby stworzyć swoje potwierdzenie. Twój kod wyglądałby więc mniej więcej tak.

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

W ten sposób jesteś niezależny od porządku, ale nadal zapewniasz, że wszystkie twoje elementy są obecne.

Jonathan
źródło
W assertArraySubsetkolejności indeksów znaczenia, więc to nie będzie działać. czyli własnym :: assertArraySubset ([ 'a'] [ 'b', 'a']) będzie fałszywe, ponieważ [0 => 'a']nie jest w środku[0 => 'b', 1 => 'a']
Robert T.
Przepraszam, ale muszę zgodzić się z Robertem. Na początku pomyślałem, że byłoby to dobre rozwiązanie do porównywania tablic z kluczami łańcuchowymi, ale assertEqualsjuż radzi sobie z tym, jeśli klucze nie są w tej samej kolejności. Właśnie to przetestowałem.
Kodos Johnson