Come testare l'equivalenza delle mappe a Golang?


92

Ho un test case basato su tabelle come questo:

func CountWords(s string) map[string]int

func TestCountWords(t *testing.T) {
  var tests = []struct {
    input string
    want map[string]int
  }{
    {"foo", map[string]int{"foo":1}},
    {"foo bar foo", map[string]int{"foo":2,"bar":1}},
  }
  for i, c := range tests {
    got := CountWords(c.input)
    // TODO test whether c.want == got
  }
}

Potrei controllare se le lunghezze sono le stesse e scrivere un ciclo che controlla se ogni coppia chiave-valore è la stessa. Ma poi devo scrivere di nuovo questo controllo quando voglio usarlo per un altro tipo di mappa (diciamomap[string]string ).

Quello che ho finito per fare è che ho convertito le mappe in stringhe e ho confrontato le stringhe:

func checkAsStrings(a,b interface{}) bool {
  return fmt.Sprintf("%v", a) != fmt.Sprintf("%v", b) 
}

//...
if checkAsStrings(got, c.want) {
  t.Errorf("Case #%v: Wanted: %v, got: %v", i, c.want, got)
}

Ciò presuppone che le rappresentazioni di stringa delle mappe equivalenti siano le stesse, il che sembra essere vero in questo caso (se le chiavi sono le stesse, hanno lo stesso valore, quindi i loro ordini saranno gli stessi). C'è un modo migliore per farlo? Qual è il modo idiomatico per confrontare due mappe nei test basati su tabelle?


4
Ehm, no: non è garantito che l'ordine di iterazione di una mappa sia prevedibile : "L'ordine di iterazione sulle mappe non è specificato e non è garantito che sia lo stesso da un'iterazione all'altra. ..." .
zzzz

2
Inoltre, per mappe di determinate dimensioni, Go procederà intenzionalmente a randomizzare l'ordine. È altamente consigliabile non dipendere da quell'ordine.
Jeremy Wall

Cercare di confrontare una mappa è un difetto di progettazione nel tuo programma.
Inanc Gumus

4
Si noti che con go 1.12 (febbraio 2019), le mappe vengono ora stampate in ordine di chiave per facilitare i test . Vedi la mia risposta di seguito
VonC

Risposte:


174

La libreria Go ti ha già coperto. Fai questo:

import "reflect"
// m1 and m2 are the maps we want to compare
eq := reflect.DeepEqual(m1, m2)
if eq {
    fmt.Println("They're equal.")
} else {
    fmt.Println("They're unequal.")
}

Se guardi il codice sorgente di reflect.DeepEqualsMap caso di, vedrai che prima controlla se entrambe le mappe sono nulle, poi controlla se hanno la stessa lunghezza prima di verificare infine se hanno lo stesso set di (key, valore) coppie.

Poiché reflect.DeepEqualaccetta un tipo di interfaccia, funzionerà su qualsiasi mappa valida ( map[string]bool, map[struct{}]interface{}, ecc.). Nota che funzionerà anche su valori non mappa, quindi fai attenzione che ciò che stai passando ad esso sono in realtà due mappe. Se gli passi due numeri interi, ti dirà felicemente se sono uguali.


Fantastico, è esattamente quello che stavo cercando. Immagino che come jnml stesse dicendo che non è così performante, ma chi se ne frega in un caso di prova.
andras

Sì, se lo desideri per un'applicazione di produzione, preferirei sicuramente una funzione scritta su misura, se possibile, ma questo fa sicuramente il trucco se le prestazioni non sono un problema.
joshlf

1
@andras Dovresti anche controllare gocheck . Semplice come c.Assert(m1, DeepEquals, m2). La cosa bella è che interrompe il test e ti dice cosa hai ottenuto e cosa ti aspettavi nell'output.
Luca

8
Vale la pena notare che DeepEqual richiede che anche l'ORDINE delle fette sia uguale .
Xeoncross


13

Qual è il modo idiomatico per confrontare due mappe nei test guidati da tabelle?

Hai il progetto go-test/deepda aiutare.

Ma: questo dovrebbe essere più semplice con Go 1.12 (febbraio 2019) in modo nativo : vedere le note di rilascio .

fmt.Sprint(map1) == fmt.Sprint(map2)

fmt

Le mappe vengono ora stampate in ordine di chiave per facilitare i test .

Le regole di ordinazione sono:

  • Quando applicabile, zero confronta basso
  • int, float e stringhe ordina per <
  • NaN confronta meno di float non NaN
  • boolconfronta falseprimatrue
  • Complesso confronta reale, quindi immaginario
  • I puntatori vengono confrontati in base all'indirizzo macchina
  • I valori dei canali vengono confrontati in base all'indirizzo macchina
  • Le strutture confrontano ogni campo a turno
  • Gli array confrontano ogni elemento a turno
  • I valori dell'interfaccia vengono confrontati prima reflect.Typedescrivendo il tipo di calcestruzzo e poi in base al valore concreto come descritto nelle regole precedenti.

Durante la stampa di mappe, i valori chiave non riflessivi come NaN venivano precedentemente visualizzati come <nil>. A partire da questa versione, vengono stampati i valori corretti.

Fonti:

Il CL aggiunge: ( CL sta per "Change List" )

Per fare ciò, aggiungiamo un pacchetto alla radice,internal/fmtsort che implementa un meccanismo generale per ordinare le chiavi della mappa indipendentemente dal loro tipo.

Questo è un po 'disordinato e probabilmente lento, ma la stampa formattata delle mappe non è mai stata veloce ed è già sempre guidata dalla riflessione.

Il nuovo pacchetto è interno perché non vogliamo che tutti lo usino per ordinare le cose. È lento, non generale e adatto solo per il sottoinsieme di tipi che possono essere chiavi di mappa.

Usa anche il pacchetto in text/template, che aveva già una versione più debole di questo meccanismo.

Puoi vedere quello usato in src/fmt/print.go#printValue(): case reflect.Map:


Scusa la mia ignoranza, sono nuovo su Go, ma in che modo esattamente questo nuovo fmtcomportamento aiuta a testare l'equivalenza delle mappe? Stai suggerendo di confrontare le rappresentazioni di stringa invece di utilizzare DeepEqual?
sschuberth

@sschuberth DeepEqualè ancora buono. (o megliocmp.Equal ) Il caso d'uso è più illustrato in twitter.com/mikesample/status/1084223662167711744 , come i registri diversi come indicato nel numero originale: github.com/golang/go/issues/21095 . Significato: a seconda della natura del test, un diff affidabile può essere d'aiuto.
VonC

fmt.Sprint(map1) == fmt.Sprint(map2)per il tl; dr
425nesp

@ 425nesp Grazie. Ho modificato la risposta di conseguenza.
VonC

11

Questo è quello che farei (codice non testato):

func eq(a, b map[string]int) bool {
        if len(a) != len(b) {
                return false
        }

        for k, v := range a {
                if w, ok := b[k]; !ok || v != w {
                        return false
                }
        }

        return true
}

OK, ma ho un altro caso di test in cui voglio confrontare le istanze di map[string]float64. eqfunziona solo per le map[string]intmappe. Devo implementare una versione della eqfunzione ogni volta che voglio confrontare istanze di un nuovo tipo di mappa?
andras

@andras: 11 SLOC. Lo specializzerei in "copia incolla" in un tempo più breve di quello necessario per chiedere informazioni su questo argomento. Tuttavia, molti altri userebbero "riflettere" per fare lo stesso, ma si tratta di prestazioni molto peggiori.
zzzz

1
non si aspetta che le mappe siano nello stesso ordine? Quale go non garantisce la visualizzazione di "Iteration order" su blog.golang.org/go-maps-in-action
nathj07

3
@ nathj07 No, perché iteriamo solo attraverso a.
Torsten Bronger

5

Disclaimer : non correlato amap[string]int ma correlata al test dell'equivalenza delle mappe in Go, che è il titolo della domanda

Se si dispone di una mappa di un tipo di puntatore (come map[*string]int), non si desidera utilizzare riflettere.DeepEqual perché restituirà false.

Infine, se la chiave è un tipo che contiene un puntatore non esportato, come time.Time, allora Reflect.DeepEqual su tale mappa può anche restituire false .


3

Utilizza il metodo "Diff" di github.com/google/go-cmp/cmp :

Codice:

// Let got be the hypothetical value obtained from some logic under test
// and want be the expected golden data.
got, want := MakeGatewayInfo()

if diff := cmp.Diff(want, got); diff != "" {
    t.Errorf("MakeGatewayInfo() mismatch (-want +got):\n%s", diff)
}

Produzione:

MakeGatewayInfo() mismatch (-want +got):
  cmp_test.Gateway{
    SSID:      "CoffeeShopWiFi",
-   IPAddress: s"192.168.0.2",
+   IPAddress: s"192.168.0.1",
    NetMask:   net.IPMask{0xff, 0xff, 0x00, 0x00},
    Clients: []cmp_test.Client{
        ... // 2 identical elements
        {Hostname: "macchiato", IPAddress: s"192.168.0.153", LastSeen: s"2009-11-10 23:39:43 +0000 UTC"},
        {Hostname: "espresso", IPAddress: s"192.168.0.121"},
        {
            Hostname:  "latte",
-           IPAddress: s"192.168.0.221",
+           IPAddress: s"192.168.0.219",
            LastSeen:  s"2009-11-10 23:00:23 +0000 UTC",
        },
+       {
+           Hostname:  "americano",
+           IPAddress: s"192.168.0.188",
+           LastSeen:  s"2009-11-10 23:03:05 +0000 UTC",
+       },
    },
  }

2

Utilizza invece cmp ( https://github.com/google/go-cmp ):

if !cmp.Equal(src, expectedSearchSource) {
    t.Errorf("Wrong object received, got=%s", cmp.Diff(expectedSearchSource, src))
}

Test fallito

Non riesce ancora quando l '"ordine" della mappa nell'output atteso non è quello restituito dalla funzione. Tuttavia, cmpè ancora in grado di indicare dove sia l'incongruenza.

Per riferimento, ho trovato questo tweet:

https://twitter.com/francesc/status/885630175668346880?lang=en

"L'utilizzo di Reflect.DeepEqual nei test è spesso una cattiva idea, ecco perché abbiamo aperto http://github.com/google/go-cmp " - Joe Tsai


1

Modo più semplice:

    assert.InDeltaMapValues(t, got, want, 0.0, "Word count wrong. Got %v, want %v", got, want)

Esempio:

import (
    "github.com/stretchr/testify/assert"
    "testing"
)

func TestCountWords(t *testing.T) {
    got := CountWords("hola hola que tal")

    want := map[string]int{
        "hola": 2,
        "que": 1,
        "tal": 1,
    }

    assert.InDeltaMapValues(t, got, want, 0.0, "Word count wrong. Got %v, want %v", got, want)
}

-5

Una delle opzioni è correggere rng:

rand.Reader = mathRand.New(mathRand.NewSource(0xDEADBEEF))

Scusami, ma come è correlata la tua risposta a questa domanda?
Dima Kozhevin

@DimaKozhevin golang utilizza internamente rng per mescolare l'ordine delle voci in una mappa. Se aggiusti l'RNG otterrai un ordine prevedibile a scopo di test.
Grozz

@ Grozz Lo fa? Perché!? Non sto necessariamente discutendo che potrebbe (non ne ho idea), semplicemente non vedo perché dovrebbe.
msanford

Non lavoro su Golang, quindi non posso spiegare il loro ragionamento, ma questo è un comportamento confermato almeno a partire dalla v1.9. Tuttavia ho visto alcune spiegazioni sulla falsariga di "vogliamo imporre che non puoi dipendere dall'ordinamento nelle mappe, perché non dovresti".
Grozz
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.