Bagaimana cara menguji kesetaraan peta di Golang?


92

Saya memiliki kasus uji berbasis tabel seperti ini:

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

Saya dapat memeriksa apakah panjangnya sama dan menulis loop yang memeriksa apakah setiap pasangan nilai kunci sama. Tapi kemudian saya harus menulis centang ini lagi ketika saya ingin menggunakannya untuk jenis peta lain (katakanlah map[string]string).

Apa yang akhirnya saya lakukan adalah, saya mengonversi peta menjadi string dan membandingkan string:

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

Ini mengasumsikan bahwa representasi string dari peta yang setara adalah sama, yang tampaknya benar dalam kasus ini (jika kuncinya sama maka mereka memiliki nilai yang sama, sehingga urutannya akan sama). Apakah ada cara yang lebih baik untuk melakukan ini? Apa cara idiomatik untuk membandingkan dua peta dalam pengujian berbasis tabel?


4
Err, no: Urutan iterasi peta tidak dijamin dapat diprediksi : "Urutan iterasi di atas peta tidak ditentukan dan tidak dijamin sama dari satu iterasi ke iterasi berikutnya. ..." .
zzzz

2
Selanjutnya untuk map dengan ukuran tertentu Go akan sengaja mengacak urutannya. Sangat disarankan untuk tidak bergantung pada pesanan itu.
Jeremy Wall

Mencoba membandingkan peta adalah cacat desain dalam program Anda.
Inanc Gumus

4
Perhatikan bahwa dengan go 1.12 (Feb. 2019), Peta sekarang dicetak dalam urutan urutan kunci untuk memudahkan pengujian . Lihat jawaban saya di bawah ini
VonC

Jawaban:


174

Perpustakaan Go telah membantu Anda. Melakukan hal ini:

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

Jika Anda melihat kode sumber untuk reflect.DeepEqual'sMap kasus, Anda akan melihat bahwa itu cek pertama jika kedua peta yang nihil, maka cek jika mereka memiliki panjang yang sama sebelum akhirnya memeriksa untuk melihat apakah mereka memiliki set yang sama (kunci, nilai) pasangan.

Karena reflect.DeepEqualmenggunakan tipe antarmuka, ini akan bekerja pada peta yang valid ( map[string]bool, map[struct{}]interface{}, dll). Perhatikan bahwa ini juga akan berfungsi pada nilai non-peta, jadi berhati-hatilah karena apa yang Anda teruskan ke sana sebenarnya adalah dua peta. Jika Anda memberikan dua bilangan bulat, itu akan dengan senang hati memberi tahu Anda apakah keduanya sama.


Luar biasa, itulah yang saya cari. Saya kira seperti yang dikatakan jnml itu bukan sebagai performant, tapi siapa yang peduli dalam kasus uji.
andras

Ya, jika Anda pernah menginginkan ini untuk aplikasi produksi, saya pasti akan menggunakan fungsi yang ditulis khusus jika memungkinkan, tetapi ini pasti berhasil jika kinerja bukan masalah.
joshlf

1
@andras Anda juga harus memeriksa gocheck . Sesederhana c.Assert(m1, DeepEquals, m2). Apa yang menyenangkan tentang ini adalah membatalkan pengujian dan memberi tahu Anda apa yang Anda dapatkan dan apa yang Anda harapkan dalam output.
Lukas

8
Perlu dicatat bahwa DeepEqual juga membutuhkan ORDER irisan yang sama .
Xeoncross


13

Apa cara idiomatik untuk membandingkan dua peta dalam pengujian berbasis tabel?

Anda memiliki proyek go-test/deepuntuk membantu.

Tetapi: ini seharusnya lebih mudah dengan Go 1.12 (Februari 2019) secara native : Lihat catatan rilis .

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

fmt

Peta sekarang dicetak dalam urutan kunci untuk memudahkan pengujian .

Aturan pemesanannya adalah:

  • Jika berlaku, nol membandingkan rendah
  • int, float, dan string yang diurutkan berdasarkan <
  • NaN membandingkan kurang dari float non-NaN
  • boolmembandingkan falsesebelumnyatrue
  • Kompleks membandingkan nyata, kemudian imajiner
  • Pointer dibandingkan dengan alamat mesin
  • Nilai saluran dibandingkan dengan alamat mesin
  • Struktur membandingkan setiap bidang secara bergantian
  • Array membandingkan setiap elemen secara bergantian
  • Nilai antarmuka dibandingkan terlebih dahulu dengan reflect.Typemendeskripsikan jenis konkretnya dan kemudian dengan nilai konkretnya seperti yang dijelaskan pada aturan sebelumnya.

Saat mencetak peta, nilai kunci non-refleksif seperti NaN sebelumnya ditampilkan sebagai <nil>. Pada rilis ini, nilai yang benar dicetak.

Sumber:

CL menambahkan: ( CL adalah singkatan dari "Change List" )

Untuk melakukan ini, kami menambahkan paket di rootinternal/fmtsort , yang mengimplementasikan mekanisme umum untuk mengurutkan kunci peta apa pun tipenya.

Ini sedikit berantakan dan mungkin lambat, tetapi pencetakan peta yang diformat tidak pernah secepat dan selalu didorong oleh refleksi.

Paket baru bersifat internal karena kami benar-benar tidak ingin semua orang menggunakan ini untuk memilah barang. Ini lambat, tidak umum, dan hanya cocok untuk subset tipe yang bisa berupa kunci peta.

Gunakan juga paket di text/template, yang sudah memiliki versi yang lebih lemah dari mekanisme ini.

Anda dapat melihat bahwa digunakan dalam src/fmt/print.go#printValue(): case reflect.Map:


Maaf atas ketidaktahuan saya, saya baru mengenal Go, tetapi bagaimana sebenarnya fmtperilaku baru ini membantu menguji kesetaraan peta? Apakah Anda menyarankan untuk membandingkan representasi string daripada menggunakan DeepEqual?
sschuberth

@subtanyarl DeepEqualmasih bagus. (atau lebih tepatnyacmp.Equal ) Kasus penggunaan lebih diilustrasikan di twitter.com/mikesample/status/1084223662167711744 , seperti log yang berbeda seperti yang dinyatakan dalam masalah asli: github.com/golang/go/issues/21095 . Artinya: bergantung pada sifat pengujian Anda, perbedaan yang andal dapat membantu.
VonC

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

@ 425nesp Terima kasih. Saya telah mengedit jawabannya sesuai dengan itu.
VonC

11

Inilah yang akan saya lakukan (kode belum teruji):

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
}

Oke, tapi saya punya kasus uji lain tempat saya ingin membandingkan contoh map[string]float64. eqhanya berfungsi untuk map[string]intpeta. Haruskah saya menerapkan versi eqfungsi setiap kali saya ingin membandingkan instance dari tipe peta baru?
andras

@andras: 11 JAM. Saya akan "menyalin tempel" mengkhususkannya dalam waktu yang lebih singkat daripada yang diperlukan untuk menanyakan tentang ini. Meskipun, banyak orang lain akan menggunakan "refleksi" untuk melakukan hal yang sama, tetapi itu jauh lebih buruk kinerja.
zzzz

1
bukankah itu mengharapkan peta berada dalam urutan yang sama? Yang pergi tidak menjamin melihat "Urutan Iterasi" di blog.golang.org/go-maps-in-action
nathj07

3
@ nathj07 Tidak, karena kami hanya melakukan iterasi a.
Torsten Bronger

5

Penafian : Tidak map[string]intterkait tetapi terkait dengan pengujian kesetaraan peta di Go, yang merupakan judul pertanyaan

Jika Anda memiliki peta tipe pointer (seperti map[*string]int), maka Anda jangan tidak ingin menggunakan reflect.DeepEqual karena akan kembali palsu.

Terakhir, jika kuncinya adalah tipe yang berisi penunjuk yang tidak diekspor, seperti time.Time, maka reflect.DeepEqual pada peta seperti itu juga dapat mengembalikan false .


3

Gunakan metode "Diff" dari github.com/google/go-cmp/cmp :

Kode:

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

Keluaran:

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

Gunakan cmp ( https://github.com/google/go-cmp ) sebagai gantinya:

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

Tes gagal

Itu masih gagal ketika peta "urutan" dalam output yang Anda harapkan tidak sesuai dengan fungsi yang dikembalikan. Namun, cmpmasih bisa menunjukkan di mana letak ketidakkonsistenannya.

Sebagai referensi, saya telah menemukan tweet ini:

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

"menggunakan reflect.DeepEqual dalam pengujian sering kali merupakan ide yang buruk, itulah alasan kami membuka http://github.com/google/go-cmp " - Joe Tsai


1

Cara termudah:

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

Contoh:

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

Salah satu opsinya adalah dengan memperbaiki rng:

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

Permisi, tapi bagaimana jawaban Anda terkait dengan pertanyaan ini?
Dima Kozhevin

@DimaKozhevin golang secara internal menggunakan rng untuk mencampur urutan entri di peta. Jika Anda memperbaiki rng, Anda akan mendapatkan urutan yang dapat diprediksi untuk tujuan pengujian.
Grozz

@Grozz Benarkah? Mengapa!? Saya tidak selalu membantah bahwa itu mungkin (saya tidak tahu) Saya hanya tidak mengerti mengapa itu terjadi.
msanford

Saya tidak bekerja di Golang, jadi saya tidak bisa menjelaskan alasan mereka, tapi itu adalah perilaku yang dikonfirmasi setidaknya pada v1.9. Namun saya melihat beberapa penjelasan di sepanjang baris "kami ingin menegakkan bahwa Anda tidak dapat bergantung pada pemesanan di peta, karena Anda tidak boleh".
Grozz
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.