Jak sprawdzić równoważność map w Golangu?

86

Mam przypadek testowy oparty na tabeli, taki jak ten:

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

Mógłbym sprawdzić, czy długości są takie same i napisać pętlę, która sprawdza, czy każda para klucz-wartość jest taka sama. Ale potem muszę napisać ten czek ponownie, gdy chcę go użyć do innego typu mapy (powiedzmy map[string]string).

Skończyło się na tym, że przekonwertowałem mapy na ciągi i porównałem ciągi:

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

Zakłada się, że reprezentacje łańcuchowe równoważnych map są takie same, co wydaje się być prawdą w tym przypadku (jeśli klucze są takie same, to mają tę samą wartość, więc ich kolejność będzie taka sama). Czy jest lepszy sposób, aby to zrobić? Jaki jest idiomatyczny sposób porównywania dwóch map w testach tabelarycznych?

andras
źródło
4
Err, no: kolejność iteracji mapy nie jest przewidywalna : „Kolejność iteracji na mapach nie jest określona i nie ma gwarancji, że będzie taka sama w kolejnych iteracjach. ...” .
zzzz
2
Ponadto w przypadku map o określonych rozmiarach Go celowo losuje kolejność. Zaleca się, aby nie polegać na tej kolejności.
Jeremy Wall
Próba porównania mapy jest błędem projektowym w twoim programie.
Inanc Gumus
4
Należy pamiętać, że wraz z wersją go 1.12 (luty 2019 r.) Mapy są teraz drukowane w kolejności sortowania według kluczy, aby ułatwić testowanie . Zobacz moją odpowiedź poniżej
VonC,

Odpowiedzi:

165

Biblioteka Go już Cię obejmuje. Zrób to:

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

Jeśli spojrzeć na kod źródłowy dla reflect.DeepEqual„s Mapprzypadku, zobaczysz, że najpierw sprawdza, czy obu mapach są zerowe, to sprawdza, czy mają taką samą długość, zanim w końcu sprawdzić, czy mają ten sam zestaw (klucz, wartość) pary.

Ponieważ reflect.DeepEqualprzyjmuje typ interfejsu, będzie działać na każdej prawidłowej mapie ( map[string]bool, map[struct{}]interface{}itp.). Zauważ, że będzie działać również na wartościach innych niż mapy, więc uważaj, aby to, co do niego przekazujesz, to tak naprawdę dwie mapy. Jeśli przekażesz mu dwie liczby całkowite, z radością powie ci, czy są równe.

joshlf
źródło
Świetnie, właśnie tego szukałem. Wydaje mi się, że jnml powiedział, że nie jest tak wydajny, ale kogo to obchodzi w przypadku testowym.
andras
Tak, jeśli kiedykolwiek zechcesz to dla aplikacji produkcyjnej, zdecydowanie wybrałbym niestandardową funkcję, jeśli to możliwe, ale to zdecydowanie załatwia sprawę, jeśli wydajność nie jest problemem.
joshlf
1
@andras Powinieneś także sprawdzić gocheck . Tak proste, jak c.Assert(m1, DeepEquals, m2). Fajne w tym jest to, że przerywa test i informuje, co otrzymałeś i czego się spodziewałeś na wyjściu.
Łukasz
8
Warto zauważyć, że DeepEqual wymaga również, aby KOLEJNOŚĆ plasterków była równa .
Xeoncross
13

Jaki jest idiomatyczny sposób porównywania dwóch map w testach tabelarycznych?

Masz projekt go-test/deepdo pomocy.

Ale: powinno to być łatwiejsze w wersji natywnej Go 1.12 (luty 2019) : zobacz informacje o wydaniu .

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

fmt

Mapy są teraz drukowane w kolejności sortowania według kluczy, aby ułatwić testowanie .

Zasady zamawiania to:

  • W stosownych przypadkach zero porównuje niskie
  • int, zmiennoprzecinkowe i ciągi uporządkowane według <
  • NaN porównuje mniej niż zmiennoprzecinkowe bez NaN
  • boolporównuje falsewcześniejtrue
  • Złożone porównuje rzeczywiste, a następnie urojone
  • Wskaźniki są porównywane według adresu komputera
  • Wartości kanałów są porównywane według adresu maszyny
  • Struktury po kolei porównują poszczególne pola
  • Tablice porównują po kolei każdy element
  • Wartości interfejsu są porównywane najpierw przez reflect.Typeopisanie konkretnego typu, a następnie przez konkretną wartość, zgodnie z opisem w poprzednich regułach.

Podczas drukowania map nierefleksyjne wartości kluczowe, takie jak NaN, były wcześniej wyświetlane jako <nil>. Od tej wersji drukowane są prawidłowe wartości.

Źródła:

CL dodaje: ( CL oznacza „Lista zmian” )

Aby to zrobić, dodajemy w katalogu głównym pakietinternal/fmtsort , który implementuje ogólny mechanizm sortowania kluczy map, niezależnie od ich typu.

Jest to trochę bałaganiarskie i prawdopodobnie powolne, ale sformatowane drukowanie map nigdy nie było szybkie i już zawsze opiera się na refleksjach.

Nowy pakiet jest wewnętrzny, ponieważ naprawdę nie chcemy, aby wszyscy używali go do sortowania rzeczy. Jest powolny, nie jest ogólny i nadaje się tylko do podzbioru typów, które mogą być kluczami mapowania.

Skorzystaj również z pakietu w text/template, który miał już słabszą wersję tego mechanizmu.

Możesz zobaczyć, że jest używany w src/fmt/print.go#printValue(): case reflect.Map:

VonC
źródło
Przepraszam za moją ignorancję, jestem nowy w Go, ale jak dokładnie to nowe fmtzachowanie pomaga w testowaniu równoważności map? Czy sugerujesz porównanie reprezentacji ciągów zamiast używania DeepEqual?
sschuberth
@sschuberth DeepEqualjest nadal dobry. (a raczejcmp.Equal ) Przypadek użycia jest lepiej zilustrowany na twitter.com/mikesample/status/1084223662167711744 , podobnie jak różne dzienniki, jak podano w oryginalnym wydaniu: github.com/golang/go/issues/21095 . Znaczenie: w zależności od charakteru testu, pomocna może być wiarygodna różnica.
VonC
fmt.Sprint(map1) == fmt.Sprint(map2)dla tl; dr
425nesp
@ 425nesp Dziękuję. Odpowiednio zredagowałem odpowiedź.
VonC
11

Oto co bym zrobił (nieprzetestowany kod):

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
}
zzzz
źródło
OK, ale mam inny przypadek testowy, w którym chcę porównać wystąpienia map[string]float64. eqdziała tylko w przypadku map[string]intmap. Czy powinienem zaimplementować wersję eqfunkcji za każdym razem, gdy chcę porównać wystąpienia mapy nowego typu?
andras
@andras: 11 SLOCów. Specjalizowałbym się w "kopiuj wklej" w krótszym czasie, niż potrzeba, aby o to zapytać. Chociaż wielu innych użyłoby „refleksu”, aby zrobić to samo, ale to znacznie gorsza wydajność.
zzzz
1
nie oznacza to, że mapy będą w tej samej kolejności? Które przejście nie gwarantuje, zobacz „Kolejność iteracji” na blog.golang.org/go-maps-in-action
nathj07
3
@ nathj07 Nie, ponieważ iterujemy tylko przez a.
Torsten Bronger
5

Zastrzeżenie : niezwiązane z map[string]inttestowaniem równoważności map w Go, ale związane z tym, co jest tytułem pytania

Jeśli masz mapę typu wskaźnika (jak map[*string]int), to czy nie chcą korzystać reflect.DeepEqual ponieważ będzie return false.

Wreszcie, jeśli klucz jest typem zawierającym niewyeksportowany wskaźnik, na przykład time.Time, to reflekt.DeepEqual na takiej mapie może również zwrócić false .

Carl
źródło
2

Użyj metody „Diff” na github.com/google/go-cmp/cmp :

Kod:

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

Wynik:

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",
+       },
    },
  }
Jonas Felber
źródło
1

Najprostszy sposób:

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

Przykład:

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)
}
miqrc
źródło
1

Zamiast tego użyj cmp ( https://github.com/google/go-cmp ):

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

Niepowodzenie testu

Nadal kończy się niepowodzeniem, gdy „kolejność” mapy w oczekiwanym wyniku nie jest tym, co zwraca funkcja. Jednak cmpnadal jest w stanie wskazać, gdzie jest niespójność.

Dla porównania znalazłem ten tweet:

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

„Używanie reflektometrów w testach jest często złym pomysłem, dlatego otwieramy źródło http://github.com/google/go-cmp ” - Joe Tsai

ericson.cepeda
źródło
-5

Jedną z opcji jest naprawienie rng:

rand.Reader = mathRand.New(mathRand.NewSource(0xDEADBEEF))
Grozz
źródło
Przepraszam, ale jak twoja odpowiedź ma się do tego pytania?
Dima Kozhevin
@DimaKozhevin golang wewnętrznie używa rng do mieszania kolejności wpisów na mapie. Jeśli naprawisz rng, otrzymasz przewidywalną kolejność do celów testowych.
Grozz,
@Grozz To robi? Czemu!? Niekoniecznie zaprzeczam, że może (nie mam pojęcia), po prostu nie rozumiem, dlaczego miałoby to nastąpić.
msanford,
Nie pracuję na Golangu, więc nie mogę wyjaśnić ich powodów, ale jest to potwierdzone zachowanie przynajmniej od wersji 1.9. Widziałem jednak wyjaśnienie w stylu „chcemy wymusić, że nie możesz polegać na zamawianiu map, bo nie powinieneś”.
Grozz,