PHPUnit: affermare che due array sono uguali, ma l'ordine degli elementi non è importante


132

Qual è un buon modo per affermare che due matrici di oggetti sono uguali, quando l'ordine degli elementi nell'array è irrilevante o addirittura soggetto a modifiche?


Ti interessa che gli oggetti nell'array siano uguali o semplicemente che ci sia x quantità di oggetto y in entrambi gli array?
edorian

@edorian Entrambi sarebbero molto interessanti. Nel mio caso, però, esiste un solo oggetto y in ciascun array.
Koen

si prega di definire uguale . Confrontare gli hash degli oggetti ordinati di cosa hai bisogno? Probabilmente dovrai comunque ordinare gli oggetti .
Takeshin

@takeshin Equal as in ==. Nel mio caso sono oggetti di valore, quindi l'identità non è necessaria. Probabilmente potrei creare un metodo di assert personalizzato. Ciò di cui avrei bisogno in questo caso è contare il numero di elementi in ciascun array, e per ogni elemento in entrambi uguale (==) deve esistere.
Koen,

7
In realtà, su PHPUnit 3.7.24, $ this-> assertEquals afferma che l'array contiene le stesse chiavi e valori, ignorando in quale ordine.
Dereckson,

Risposte:


38

Il modo più pulito per farlo sarebbe estendere phpunit con un nuovo metodo di asserzione. Ma ecco un'idea per un modo più semplice per ora. Codice non testato, si prega di verificare:

Da qualche parte nella tua 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;
}

Nel tuo test:

$this->assertTrue(arrays_are_similar($foo, $bar));

Craig, sei vicino a quello che ho provato in origine. In realtà array_diff è ciò di cui avevo bisogno, ma non sembra funzionare per gli oggetti. Ho scritto la mia affermazione personalizzata come spiegato qui: phpunit.de/manual/current/en/extending-phpunit.html
koen,

Il collegamento corretto ora è con https e senza www: phpunit.de/manual/current/en/extending-phpunit.html
Xavi Montero

ogni parte non è necessaria - array_diff_assoc confronta già sia le chiavi che i valori. EDIT: e devi controllare count(array_diff_assoc($b, $a))anche.
JohnSmith,

212

Puoi usare il metodo assertEqualsCanonicalizing che è stato aggiunto in PHPUnit 7.5. Se si confrontano le matrici usando questo metodo, queste matrici saranno ordinate dallo stesso comparatore di matrici PHPUnit.

Esempio di codice:

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

Nelle versioni precedenti di PHPUnit è possibile utilizzare un parametro non documentato $ canonicalize del metodo assertEquals . Se passi $ canonicalize = true , otterrai lo stesso effetto:

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

Codice sorgente del comparatore di array all'ultima versione di PHPUnit: https://github.com/sebastianbergmann/comparator/blob/master/src/ArrayComparator.php#L46


10
Fantastico. Perché questa non è la risposta accettata, @koen?
Rinogo,

7
L'uso $delta = 0.0, $maxDepth = 10, $canonicalize = truedi passare parametri nella funzione è fuorviante - PHP non supporta argomenti con nome. Quello che sta effettivamente facendo è impostare queste tre variabili, quindi passare immediatamente i loro valori alla funzione. Ciò causerà problemi se queste tre variabili sono già definite nell'ambito locale poiché verranno sovrascritte.
Yi Jiang,

11
@ yi-jiang, è solo il modo più breve per spiegare il significato di argomenti aggiuntivi. E 'più di auto-descrittivo variante poi più pulito: $this->assertEquals($array1, $array2, "\$canonicalize = true", 0.0, 10, true);. Potrei usare 4 righe invece di 1, ma non l'ho fatto.
pryazhnikov,

8
Non fai notare che questa soluzione eliminerà le chiavi.
Odalrick,

8
si noti che $canonicalizeverrà rimosso: github.com/sebastianbergmann/phpunit/issues/3342 e assertEqualsCanonicalizing()lo sostituirà.
Koen

35

Il mio problema era che avevo 2 array (le chiavi dell'array non sono rilevanti per me, ma solo i valori).

Ad esempio, volevo testare se

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

aveva lo stesso contenuto (ordine non pertinente per me) di

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

Quindi ho usato array_diff .

Il risultato finale è stato (se gli array sono uguali, la differenza si tradurrà in un array vuoto). Si noti che la differenza viene calcolata in entrambi i modi (grazie @beret, @GordonM)

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

Per un messaggio di errore più dettagliato (durante il debug), puoi anche provare in questo modo (grazie @ DenilsonSá):

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

Vecchia versione con bug interni:

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


Il problema di questo approccio è che se $array1ha più valori di $array2, allora restituisce array vuoto anche se i valori dell'array non sono uguali. Dovresti anche testare, quella dimensione dell'array è la stessa, per essere sicuro.
Petrkotek,

3
Dovresti fare array_diff o array_diff_assoc in entrambi i modi. Se un array è un superset dell'altro, array_diff in una direzione sarà vuoto, ma non vuoto nell'altra. $a1 = [1,2,3,4,5]; $a2 = [1,3,5]; var_dump (array_diff ($a1, $a2)); var_dump (array_diff ($a2, $a1))
GordonM,

2
assertEmptynon stamperà l'array se non è vuoto, il che è scomodo durante i test di debug. Suggerirei di usare:, $this->assertSame(array_diff($expected, $actual), array_diff($actual, $expected), $message);poiché questo stamperà il messaggio di errore più utile con il minimo di codice aggiuntivo. Questo funziona perché A \ B = B \ A ⇔ A \ B e B \ A sono vuoti ⇔ A = B
Denilson Sá Maia

Si noti che array_diff converte ogni valore in stringa per il confronto.
Konstantin Pelepelin,

Per aggiungere a @checat: verrà visualizzato un Array to string conversionmessaggio quando si tenta di eseguire il cast di un array in una stringa. Un modo per aggirare il problema è usareimplode
ub3rst4r l'

20

Un'altra possibilità:

  1. Ordina entrambi gli array
  2. Convertili in una stringa
  3. Asserire che entrambe le stringhe sono uguali

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

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

$this->assertEquals(json_encode($exp), json_encode($arr));

Se uno dei due array contiene oggetti, json_encode codifica solo le proprietà pubbliche. Funzionerà comunque, ma solo se tutte le proprietà che determinano l'uguaglianza sono pubbliche. Dai un'occhiata alla seguente interfaccia per controllare json_encoding di proprietà private. php.net/manual/en/class.jsonserializable.php
Westy92

1
Funziona anche senza smistamento. Per assertEqualsl'ordine non importa.
Appassionato il

1
In effetti, possiamo anche usare $this->assertSame($exp, $arr); che fa il confronto simile come $this->assertEquals(json_encode($exp), json_encode($arr)); unica differenza è che non dobbiamo usare json_encode
Maxwells

15

Metodo di supporto semplice

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

O se sono necessarie ulteriori informazioni di debug quando gli array non sono uguali

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

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

8

Se l'array è ordinabile, li ordinerei entrambi prima di verificare l'uguaglianza. Altrimenti, li convertirei in set di qualche tipo e li confronterei.


6

Utilizzando 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 assert (più facili da leggere):

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

È intelligente :)
Christian,

Esattamente quello che stavo cercando. Semplice.
Abdul Maye,

6

Anche se non ti interessa l'ordine, potrebbe essere più semplice tenerne conto:

Provare:

asort($foo);
asort($bar);
$this->assertEquals($foo, $bar);

5

Nei nostri test utilizziamo il seguente metodo wrapper:

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

5

Se le chiavi sono uguali ma non funzionanti, ciò dovrebbe risolverlo.

Devi solo ottenere le chiavi nello stesso ordine e confrontare i risultati.

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

3

Le soluzioni fornite non hanno funzionato per me perché volevo essere in grado di gestire array multidimensionali e avere un messaggio chiaro di ciò che è diverso tra i due array.

Ecco la mia funzione

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

Quindi usarlo

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

1

Ho scritto un codice semplice per ottenere prima tutte le chiavi da un array multidimensionale:

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

Quindi per verificare che fossero strutturati allo stesso modo indipendentemente dall'ordine delle chiavi:

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

HTH


0

Se i valori sono solo int o stringhe e nessuna matrice a più livelli ....

Perché non solo ordinare le matrici, convertirle in stringhe ...

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

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

... e poi confronta la stringa:

    $this->assertEquals($myExpectedArray, $myArray);

-2

Se vuoi testare solo i valori dell'array puoi fare:

$this->assertEquals(array_values($arrayOne), array_values($arrayTwo));

1
Sfortunatamente questo non sta testando "solo i valori" ma sia i valori che l'ordine dei valori. Ad esempioecho("<pre>"); print_r(array_values(array("size" => "XL", "color" => "gold"))); print_r(array_values(array("color" => "gold", "size" => "XL")));
Pocketsand,

-3

Un'altra opzione, come se non ne avessi già abbastanza, è combinare in assertArraySubsetcombinazione con assertCountper fare la tua affermazione. Quindi, il tuo codice sarebbe simile.

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

In questo modo sei indipendente dall'ordine ma affermi comunque che tutti i tuoi elementi sono presenti.


Nel assertArraySubsetl'ordine degli indici materia così non funzionerà. cioè self :: assertArraySubset (['a'], ['b', 'a']) sarà falso, perché [0 => 'a']non è dentro[0 => 'b', 1 => 'a']
Robert

Scusa ma devo concordare con Robert. All'inizio ho pensato che questa sarebbe stata una buona soluzione per confrontare gli array con le chiavi di stringa, ma assertEqualsgià lo gestisce se le chiavi non sono nello stesso ordine. L'ho appena provato.
Kodos Johnson,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.