Làm thế nào để kiểm tra tính tương đương của các bản đồ trong Golang?


86

Tôi có một trường hợp kiểm tra hướng bảng như sau:

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

Tôi có thể kiểm tra xem độ dài có giống nhau không và viết một vòng lặp để kiểm tra xem mọi cặp khóa-giá trị có giống nhau hay không. Nhưng sau đó tôi phải viết séc này một lần nữa khi tôi muốn sử dụng nó cho một loại bản đồ khác (giả sử map[string]string).

Những gì tôi đã làm là, tôi đã chuyển đổi các bản đồ thành chuỗi và so sánh các chuỗi:

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

Điều này giả định rằng các biểu diễn chuỗi của các bản đồ tương đương là giống nhau, điều này dường như đúng trong trường hợp này (nếu các khóa giống nhau thì chúng băm thành cùng một giá trị, vì vậy thứ tự của chúng sẽ giống nhau). Có cách nào tốt hơn để làm điều này? Cách thành ngữ để so sánh hai bản đồ trong các bài kiểm tra theo hướng bảng là gì?


4
Lỗi, không: Thứ tự lặp lại bản đồ không được đảm bảo là có thể dự đoán được : "Thứ tự lặp lại trên các bản đồ không được chỉ định và không được đảm bảo là giống nhau từ lần lặp này đến lần lặp tiếp theo ..." .
zzzz

2
Hơn nữa đối với các bản đồ có kích thước nhất định, Go sẽ cố ý sắp xếp thứ tự ngẫu nhiên. Bạn không nên phụ thuộc vào thứ tự đó.
Jeremy Wall

Cố gắng so sánh bản đồ là một lỗi thiết kế trong chương trình của bạn.
Inanc Gumus

4
Lưu ý rằng với ngày 1.12 (tháng 2 năm 2019), Bản đồ hiện được in theo thứ tự được sắp xếp theo khóa để dễ kiểm tra . Xem câu trả lời của tôi bên dưới
VonC

Câu trả lời:


165

Thư viện cờ vây đã có bạn. Làm cái này:

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

Nếu bạn nhìn vào các mã nguồn cho reflect.DeepEqual's Maptrường hợp, bạn sẽ thấy rằng nó sẽ kiểm tra đầu tiên nếu cả hai bản đồ là con số không, sau đó nó sẽ kiểm tra xem họ có cùng độ dài trước khi cuối cùng kiểm tra để xem nếu họ có cùng một bộ (key, giá trị) các cặp.

Bởi vì reflect.DeepEqualcó một loại giao diện, nó sẽ hoạt động trên bất kỳ bản đồ hợp lệ nào ( map[string]bool, map[struct{}]interface{}, v.v.). Lưu ý rằng nó cũng sẽ hoạt động trên các giá trị không phải bản đồ, vì vậy hãy cẩn thận rằng những gì bạn đang chuyển tới nó thực sự là hai bản đồ. Nếu bạn chuyển cho nó hai số nguyên, nó sẽ vui vẻ cho bạn biết liệu chúng có bằng nhau hay không.


Tuyệt vời, đó chính xác là những gì tôi đang tìm kiếm. Tôi đoán như jnml đã nói rằng nó không hoạt động tốt, nhưng ai quan tâm đến trường hợp thử nghiệm.
andras

Vâng, nếu bạn muốn điều này cho một ứng dụng sản xuất, tôi chắc chắn sẽ sử dụng một chức năng được viết tùy chỉnh nếu có thể, nhưng điều này chắc chắn sẽ thành công nếu hiệu suất không phải là mối quan tâm.
joshlf

1
@andras Bạn cũng nên kiểm tra gocheck . Đơn giản như c.Assert(m1, DeepEquals, m2). Điều thú vị về điều này là nó hủy bỏ bài kiểm tra và cho bạn biết bạn nhận được gì và bạn mong đợi gì ở đầu ra.
Luke

8


13

Cách thành ngữ để so sánh hai bản đồ trong các bài kiểm tra theo hướng bảng là gì?

Bạn có dự án go-test/deepđể giúp đỡ.

Nhưng: đây nên dễ dàng hơn với Go 1.12 (tháng 2 năm 2019) nguyên bản : Xem ghi chú phát hành .

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

fmt

Bản đồ hiện được in theo thứ tự được sắp xếp theo khóa để dễ kiểm tra .

Các quy tắc đặt hàng là:

  • Khi áp dụng, nil so sánh thấp
  • ints, float và string sắp xếp theo thứ tự <
  • NaN so sánh ít hơn NaN float
  • boolso sánh falsetrước đâytrue
  • Phức tạp so sánh giữa thực và ảo
  • Con trỏ so sánh theo địa chỉ máy
  • Giá trị kênh so sánh theo địa chỉ máy
  • Các cấu trúc lần lượt so sánh từng trường
  • Các mảng lần lượt so sánh từng phần tử
  • Giá trị giao diện so sánh trước tiên bằng cách reflect.Typemô tả loại cụ thể và sau đó bằng giá trị cụ thể như được mô tả trong các quy tắc trước đó.

Khi in bản đồ, các giá trị khóa không phản xạ như NaN trước đây được hiển thị dưới dạng <nil>. Kể từ bản phát hành này, các giá trị chính xác được in.

Nguồn:

CL cho biết thêm: ( CL là viết tắt của "Danh sách thay đổi" )

Để làm điều này, chúng tôi thêm một gói ở gốc,internal/fmtsort thực hiện một cơ chế chung để phân loại các khóa bản đồ bất kể loại của chúng.

Điều này hơi lộn xộn và có thể là chậm, nhưng việc in bản đồ theo định dạng chưa bao giờ nhanh và luôn hướng đến sự phản chiếu.

Gói mới là nội bộ vì chúng tôi thực sự không muốn mọi người sử dụng gói này để sắp xếp mọi thứ. Nó chậm, không chung chung và chỉ phù hợp với tập hợp con các loại có thể là khóa bản đồ.

Cũng sử dụng gói trong text/templateđó đã có phiên bản yếu hơn của cơ chế này.

Bạn có thể thấy nó được sử dụng trong src/fmt/print.go#printValue(): case reflect.Map:


Xin lỗi vì sự thiếu hiểu biết của tôi, tôi mới sử dụng cờ vây, nhưng chính xác thì fmthành vi mới này giúp kiểm tra tính tương đương của các bản đồ như thế nào? Bạn có đề xuất so sánh các biểu diễn chuỗi thay vì sử dụng DeepEqualkhông?
sschuberth

@sschuberth DeepEqualvẫn tốt. (hoặc đúng hơncmp.Equal ) Trường hợp sử dụng được minh họa rõ hơn trong twitter.com/mikesample/status/1084223662167711744 , chẳng hạn như các nhật ký khác nhau như đã nêu trong vấn đề gốc: github.com/golang/go/issues/21095 . Ý nghĩa: tùy thuộc vào bản chất của thử nghiệm của bạn, một điểm khác biệt đáng tin cậy có thể giúp ích.
VonC

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

@ 425nesp Cảm ơn bạn. Tôi đã chỉnh sửa câu trả lời cho phù hợp.
VonC

11

Đây là những gì tôi sẽ làm (mã chưa được kiểm tra):

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
}

Được, nhưng tôi có một trường hợp thử nghiệm khác mà tôi muốn so sánh các trường hợp của map[string]float64. eqchỉ hoạt động cho map[string]intbản đồ. Tôi có nên triển khai một phiên bản của eqhàm mỗi khi tôi muốn so sánh các trường hợp của một loại bản đồ mới không?
andras

@andras: 11 SLOC. Tôi muốn "copy paste" chuyên môn hóa nó trong thời gian ngắn hơn cần thiết để hỏi về điều này. Mặc dù, nhiều người khác sẽ sử dụng "phản xạ" để làm điều tương tự, nhưng điều đó có hiệu suất kém hơn nhiều.
zzzz

1
điều đó không mong đợi các bản đồ theo cùng một thứ tự? Mà đi không đảm bảo xem "Iteration trật tự" trên blog.golang.org/go-maps-in-action
nathj07

3
@ nathj07 Không, vì chúng tôi chỉ lặp qua a.
Torsten Bronger

5

Tuyên bố từ chối trách nhiệm : Không liên quan map[string]intnhưng liên quan đến việc kiểm tra tính tương đương của các bản đồ trong cờ vây , đó là tiêu đề của câu hỏi

Nếu bạn có một bản đồ của một loại con trỏ (như map[*string]int), sau đó bạn làm không muốn sử dụng reflect.DeepEqual bởi vì nó sẽ trả về false.

Cuối cùng, nếu khóa là loại có chứa một con trỏ chưa được báo cáo, như time.Time, thì phản ánh.DeepEqual trên bản đồ như vậy cũng có thể trả về false .


2

Sử dụng phương pháp "Khác biệt" của github.com/google/go-cmp/cmp :

Mã:

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

Đầu ra:

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

Cách đơn giản nhất:

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

Thí dụ:

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

Sử dụng cmp ( https://github.com/google/go-cmp ) để thay thế:

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

Kiểm tra không thành công

Nó vẫn không thành công khi "thứ tự" bản đồ trong kết quả mong đợi của bạn không phải là những gì hàm của bạn trả về. Tuy nhiên, cmpvẫn có thể chỉ ra sự không nhất quán là ở đâu.

Để tham khảo, tôi đã tìm thấy dòng tweet này:

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

"sử dụng phản xạ.DeepEqual trong các thử nghiệm thường là một ý tưởng tồi, đó là lý do tại sao chúng tôi mở http://github.com/google/go-cmp có nguồn gốc " - Joe Tsai


-5

Một trong những tùy chọn là sửa lỗi rng:

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

Xin lỗi, nhưng câu trả lời của bạn liên quan đến câu hỏi này như thế nào?
Dima Kozhevin

@DimaKozhevin golang sử dụng nội bộ rng để kết hợp thứ tự các mục nhập trong bản đồ. Nếu bạn sửa lỗi rng, bạn sẽ nhận được một thứ tự có thể đoán trước cho mục đích thử nghiệm.
Grozz

@Grozz Nó không? Tại sao!? Tôi không nhất thiết phải tranh cãi rằng nó có thể (tôi không biết) Tôi chỉ không hiểu tại sao nó lại như vậy.
msanford

Tôi không làm việc trên Golang, vì vậy tôi không thể giải thích lý do của họ, nhưng đó là một hành vi đã được xác nhận ít nhất là kể từ v1.9. Tuy nhiên, tôi đã thấy một số giải thích dọc theo dòng "chúng tôi muốn thực thi rằng bạn không thể phụ thuộc vào việc đặt hàng trong bản đồ, bởi vì bạn không nên".
Grozz
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.