Wie teste ich die Äquivalenz von Karten in Golang?


86

Ich habe einen tischgesteuerten Testfall wie diesen:

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

Ich könnte überprüfen, ob die Längen gleich sind, und eine Schleife schreiben, die prüft, ob jedes Schlüssel-Wert-Paar gleich ist. Aber dann muss ich diesen Scheck noch einmal schreiben, wenn ich ihn für einen anderen Kartentyp verwenden möchte (sagen wir map[string]string).

Am Ende habe ich die Maps in Strings konvertiert und die Strings verglichen:

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

Dies setzt voraus, dass die Zeichenfolgendarstellungen äquivalenter Karten identisch sind, was in diesem Fall der Fall zu sein scheint (wenn die Schlüssel gleich sind, haben sie den gleichen Wert, sodass ihre Reihenfolge gleich ist). Gibt es einen besseren Weg, dies zu tun? Was ist die idiomatische Methode, um zwei Karten in tabellengesteuerten Tests zu vergleichen?


4
Err, nein: Die Reihenfolge, in der eine Karte iteriert wird, ist nicht vorhersehbar : "Die Iterationsreihenfolge über Karten ist nicht angegeben und von einer Iteration zur nächsten garantiert nicht dieselbe. ..." .
zzzz

2
Bei Karten bestimmter Größen wird Go die Reihenfolge absichtlich zufällig festlegen. Es ist sehr ratsam, sich nicht auf diese Reihenfolge zu verlassen.
Jeremy Wall

Der Versuch, eine Karte zu vergleichen, ist ein Konstruktionsfehler in Ihrem Programm.
Inanc Gumus

4
Beachten Sie, dass mit go 1.12 (Februar 2019) die Karten jetzt in schlüsselsortierter Reihenfolge gedruckt werden, um das Testen zu vereinfachen . Siehe meine Antwort unten
VonC

Antworten:


165

In der Go-Bibliothek sind Sie bereits vertreten. Mach das:

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

Wenn Sie sich den Quellcode für reflect.DeepEqualden MapFall ansehen , werden Sie feststellen, dass zuerst überprüft wird, ob beide Karten Null sind, und dann überprüft wird, ob sie dieselbe Länge haben, bevor schließlich überprüft wird, ob sie denselben Satz von (Schlüssel, Wert) Paare.

Da reflect.DeepEqualein Schnittstellentyp verwendet wird, funktioniert er auf jeder gültigen Karte ( map[string]bool, map[struct{}]interface{}usw.). Beachten Sie, dass dies auch für Nicht-Kartenwerte funktioniert. Achten Sie also darauf, dass es sich bei der Übergabe tatsächlich um zwei Karten handelt. Wenn Sie zwei Ganzzahlen übergeben, wird Ihnen gerne mitgeteilt, ob sie gleich sind.


Genial, genau das habe ich gesucht. Ich denke, als jnml sagte, es sei nicht so performant, aber wen interessiert das in einem Testfall?
Andras

Ja, wenn Sie dies jemals für eine Produktionsanwendung wünschen, würde ich definitiv eine benutzerdefinierte Funktion verwenden, wenn dies möglich ist, aber dies ist definitiv der Trick, wenn die Leistung keine Rolle spielt.
Joshlf

1
@andras Du solltest dir auch gocheck ansehen . So einfach wie c.Assert(m1, DeepEquals, m2). Das Schöne daran ist, dass der Test abgebrochen wird und Sie erfahren, was Sie erhalten haben und was Sie in der Ausgabe erwartet haben.
Luke

8
Es ist erwähnenswert, dass DeepEqual auch erfordert , dass die BESTELLUNG der Slices gleich ist .
Xeoncross


13

Was ist die idiomatische Methode, um zwei Karten in tabellengesteuerten Tests zu vergleichen?

Sie haben das Projekt go-test/deepzu helfen.

Aber: Dies sollte mit Go 1.12 (Februar 2019) nativ einfacher sein : Siehe Versionshinweise .

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

fmt

Karten werden jetzt in schlüsselsortierter Reihenfolge gedruckt, um das Testen zu vereinfachen .

Die Bestellregeln sind:

  • Wenn zutreffend, vergleicht Null niedrig
  • Ints, Floats und Strings werden nach sortiert <
  • NaN vergleicht weniger als Nicht-NaN-Floats
  • boolvergleicht falsevorhertrue
  • Komplex vergleicht real, dann imaginär
  • Zeiger vergleichen nach Maschinenadresse
  • Kanalwerte werden nach Maschinenadresse verglichen
  • Strukturen vergleichen nacheinander jedes Feld
  • Arrays vergleichen nacheinander jedes Element
  • Schnittstellenwerte werden zuerst verglichen, indem reflect.Typeder konkrete Typ beschrieben wird, und dann anhand des konkreten Werts, wie in den vorherigen Regeln beschrieben.

Beim Drucken von Karten wurden zuvor nichtreflexive Schlüsselwerte wie NaN als angezeigt <nil>. Ab dieser Version werden die richtigen Werte gedruckt.

Quellen:

Der CL fügt hinzu: ( CL steht für "Change List" )

Dazu fügen wir im Stammverzeichnis ein Paket hinzuinternal/fmtsort , das einen allgemeinen Mechanismus zum Sortieren von Kartenschlüsseln unabhängig von ihrem Typ implementiert.

Dies ist ein wenig chaotisch und wahrscheinlich langsam, aber das formatierte Drucken von Karten war noch nie schnell und ist bereits immer reflexionsgetrieben.

Das neue Paket ist intern, weil wir wirklich nicht wollen, dass jeder, der dies verwendet, Dinge sortiert. Es ist langsam, nicht allgemein und nur für die Teilmenge der Typen geeignet, bei denen es sich um Kartenschlüssel handeln kann.

Verwenden Sie auch das Paket in text/template, das bereits eine schwächere Version dieses Mechanismus hatte.

Sie können sehen, dass in verwendet src/fmt/print.go#printValue(): case reflect.Map:


Entschuldigung für meine Unwissenheit, ich bin neu in Go, aber wie genau hilft dieses neue fmtVerhalten, die Gleichwertigkeit von Karten zu testen? Schlagen Sie vor, die Zeichenfolgendarstellungen zu vergleichen, anstatt sie zu verwenden DeepEqual?
Sschuberth

@sschuberth DeepEqualist immer noch gut. (oder besser gesagtcmp.Equal ) Der Anwendungsfall wird in twitter.com/mikesample/status/1084223662167711744 näher erläutert , wie bei unterschiedlichen Protokollen, wie in der Originalausgabe angegeben: github.com/golang/go/issues/21095 . Das heißt: Abhängig von der Art Ihres Tests kann ein zuverlässiger Unterschied hilfreich sein.
VonC

fmt.Sprint(map1) == fmt.Sprint(map2)für die tl; dr
425nesp

@ 425nesp Danke. Ich habe die Antwort entsprechend bearbeitet.
VonC

11

Folgendes würde ich tun (ungetesteter Code):

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, aber ich habe einen anderen Testfall, in dem ich Instanzen von vergleichen möchte map[string]float64. eqfunktioniert nur für map[string]intKarten. Sollte ich eqjedes Mal eine Version der Funktion implementieren, wenn ich Instanzen eines neuen Kartentyps vergleichen möchte?
Andras

@andras: 11 SLOCs. Ich würde "Kopieren Einfügen" in kürzerer Zeit spezialisieren, als es nötig ist, um danach zu fragen. Zwar würden viele andere "Reflect" verwenden, um dasselbe zu tun, aber das ist eine viel schlechtere Leistung.
zzzz

1
Erwartet das nicht, dass die Karten in derselben Reihenfolge sind? Was geht, garantiert nicht "Iterationsreihenfolge" auf blog.golang.org/go-maps-in-action
nathj07

3
@ nathj07 Nein, weil wir nur durchlaufen a.
Torsten Bronger

5

Haftungsausschluss : Nicht im map[string]intZusammenhang mit dem Testen der Äquivalenz von Karten in Go, dem Titel der Frage

Wenn Sie eine Karte eines Zeigertyp (wie haben map[*string]int), dann Sie haben nicht wollen reflect.DeepEqual verwenden , weil es falsch zurück.

Wenn der Schlüssel ein Typ ist, der einen nicht exportierten Zeiger enthält, wie z. B. time.Time, reflektieren Sie. DepepEqual auf einer solchen Karte kann auch false zurückgeben .


2

Verwenden Sie die "Diff" -Methode von github.com/google/go-cmp/cmp :

Code:

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

Ausgabe:

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",
+       },
    },
  }

1

Einfachster Weg:

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

Beispiel:

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

1

Verwenden Sie stattdessen cmp ( https://github.com/google/go-cmp ):

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

Test fehlgeschlagen

Es schlägt immer noch fehl, wenn die "Reihenfolge" der Karte in Ihrer erwarteten Ausgabe nicht dem entspricht, was Ihre Funktion zurückgibt. Kann cmpjedoch immer noch darauf hinweisen, wo die Inkonsistenz liegt.

Als Referenz habe ich diesen Tweet gefunden:

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

"Die Verwendung von Reflect.DeepEqual in Tests ist oft eine schlechte Idee. Deshalb öffnen wir http://github.com/google/go-cmp " - Joe Tsai


-5

Eine der Optionen ist das Beheben von rng:

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

Entschuldigung, aber wie hängt Ihre Antwort mit dieser Frage zusammen?
Dima Kozhevin

@DimaKozhevin golang verwendet intern rng, um die Reihenfolge der Einträge in einer Karte zu mischen. Wenn Sie das Problem beheben, erhalten Sie zu Testzwecken eine vorhersehbare Bestellung.
Grozz

@Grozz Es tut? Warum!? Ich bestreite nicht unbedingt, dass es sein könnte (ich habe keine Ahnung). Ich verstehe einfach nicht, warum es so sein würde.
Msanford

Ich arbeite nicht an Golang, daher kann ich ihre Argumentation nicht erklären, aber das ist zumindest ab Version 1.1 ein bestätigtes Verhalten. Ich sah jedoch eine Erklärung in der Art von "Wir möchten durchsetzen, dass Sie sich nicht auf die Bestellung in Karten verlassen können, weil Sie es nicht sollten".
Grozz
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.