如何在Golang中测试地图的等价性?

huangapple go评论80阅读模式
英文:

How to test the equivalence of maps in Golang?

问题

我有一个类似这样的基于表的测试用例:

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

我可以检查长度是否相同,并编写一个循环来检查每个键值对是否相同。但是,当我想要将其用于另一种类型的映射(比如map[string]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)
}

这假设等价映射的字符串表示相同,在这种情况下似乎是正确的(如果键相同,则它们的哈希值相同,因此它们的顺序也将相同)。有没有更好的方法来做到这一点?在基于表的测试中,比较两个映射的惯用方法是什么?

英文:

I have a table-driven test case like this one:

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

I could check whether the lengths are the same and write a loop that checks if every key-value pair is the same. But then I have to write this check again when I want to use it for another type of map (say map[string]string).

What I ended up doing is, I converted the maps to strings and compared the strings:

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

This assumes that the string representations of equivalent maps are the same, which seems to be true in this case (if the keys are the same then they hash to the same value, so their orders will be the same). Is there a better way to do this? What is the idiomatic way to compare two maps in table-driven tests?

答案1

得分: 250

Go语言的库已经为你提供了解决方案。按照以下步骤操作:

import "reflect"

// m1和m2是我们想要比较的两个map
eq := reflect.DeepEqual(m1, m2)
if eq {
    fmt.Println("它们相等。")
} else {
    fmt.Println("它们不相等。")
}

如果你查看reflect.DeepEqual的源代码中关于Map的部分,你会发现它首先检查两个map是否都为nil,然后检查它们是否具有相同的长度,最后检查它们是否具有相同的键值对集合。

由于reflect.DeepEqual接受一个接口类型,它可以用于任何有效的map(例如map[string]boolmap[struct{}]interface{}等)。请注意,它也可以用于非map值,所以要小心确保你传递给它的是两个map。如果你传递给它两个整数,它会高兴地告诉你它们是否相等。

英文:

The Go library has already got you covered. Do this:

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

If you look at the source code for reflect.DeepEqual's Map case, you'll see that it first checks if both maps are nil, then it checks if they have the same length before finally checking to see if they have the same set of (key, value) pairs.

Because reflect.DeepEqual takes an interface type, it will work on any valid map (map[string]bool, map[struct{}]interface{}, etc). Note that it will also work on non-map values, so be careful that what you're passing to it are really two maps. If you pass it two integers, it will happily tell you whether they are equal.

答案2

得分: 22

在表驱动测试中,比较两个映射的惯用方法是什么?

你可以使用项目go-test/deep来帮助实现。

但是:从Go 1.12(2019年2月)开始,这将更加容易地natively实现:请参阅发布说明

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

fmt

为了方便测试,映射现在按键排序顺序打印

排序规则如下:

  • 如果适用,nil比较低
  • 整数、浮点数和字符串按<顺序排列
  • NaN比非NaN浮点数小
  • booltrue之前比较false
  • 复数按实部、虚部比较
  • 指针按机器地址比较
  • 通道值按机器地址比较
  • 结构体按顺序比较每个字段
  • 数组按顺序比较每个元素
  • 接口值首先按描述具体类型的reflect.Type比较,然后按前面的规则比较具体值。

在打印映射时,以前会将非自反键值(如NaN)显示为<nil>。从此版本开始,将打印正确的值。

来源:

该CL添加了:(CL代表“Change List”)

为此,我们添加了一个位于根目录的包internal/fmtsort,它实现了一种通用机制,用于对映射键进行排序,而不考虑其类型。

这有点混乱,可能也很慢,但是映射的格式化打印从来都不快,并且已经始终是反射驱动的。

新包是internal的,因为我们真的不希望每个人都使用它来对事物进行排序。它很慢,不通用,并且只适用于可以作为映射键的类型的子集。

text/template包中也使用了这个包,该包已经有一个较弱版本的这个机制。

你可以在src/fmt/print.go#printValue(): case reflect.Map:中看到它的使用。

英文:

> What is the idiomatic way to compare two maps in table-driven tests?

You have the project go-test/deep to help.

But: this should be easier with Go 1.12 (February 2019) natively: See release notes.

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

> ## fmt

> Maps are now printed in key-sorted order to ease testing.
>
> The ordering rules are:
>
> - When applicable, nil compares low
> - ints, floats, and strings order by &lt;
> - NaN compares less than non-NaN floats
> - bool compares false before true
> - Complex compares real, then imaginary
> - Pointers compare by machine address
> - Channel values compare by machine address
> - Structs compare each field in turn
> - Arrays compare each element in turn
> - Interface values compare first by reflect.Type describing the concrete type and then by concrete value as described in the previous rules.
>
> When printing maps, non-reflexive key values like NaN were previously displayed as &lt;nil&gt;. As of this release, the correct values are printed.

Sources:

The CL adds: (CL stands for "Change List")

> To do this, we add a package at the root, internal/fmtsort, that implements a general mechanism for sorting map keys regardless of their type.
>
> This is a little messy and probably slow, but formatted printing of maps has never been fast and is already always reflection-driven.
>
> The new package is internal because we really do not want everyone using this to sort things. It is slow, not general, and only suitable for the subset of types that can be map keys.

Also use the package in text/template, which already had a weaker version of this mechanism.

You can see that used in src/fmt/print.go#printValue(): case reflect.Map:

答案3

得分: 15

这是我会做的(未经测试的代码):

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
}

这段代码是一个用于比较两个map[string]int类型的函数。它首先检查两个map的长度是否相等,如果不相等则返回false。然后,它遍历第一个map的键值对,检查第二个map中是否存在相同的键,并且对应的值也相等,如果有不满足条件的情况,则返回false。如果所有的键值对都满足条件,则返回true。

英文:

This is what I would do (untested 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
}

答案4

得分: 8

使用 cmp (https://github.com/google/go-cmp) 替代:

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

如何在Golang中测试地图的等价性?

当你期望的输出中的 "order" 映射与你的函数返回的不一致时,它仍然会失败。然而,cmp 仍然能够指出不一致的地方。

作为参考,我找到了这条推文:

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

"在测试中使用 reflect.DeepEqual 往往是一个坏主意,这就是为什么我们开源了 http://github.com/google/go-cmp" - Joe Tsai

英文:

Use cmp (https://github.com/google/go-cmp) instead:

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

如何在Golang中测试地图的等价性?

It does still fail when the map "order" in your expected output is not what your function returns. However, cmp is still able to point out where the inconsistency is.

For reference, I have found this tweet:

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

> "using reflect.DeepEqual in tests is often a bad idea, that's why we open sourced http://github.com/google/go-cmp"

  • Joe Tsai

答案5

得分: 5

免责声明:与map[string]int无关,但与测试Go中映射的等价性有关,这是问题的标题。

如果你有一个指针类型的映射(例如map[*string]int),那么你不应该使用reflect.DeepEqual,因为它会返回false。

最后,如果键是包含未导出指针的类型,比如time.Time,那么在这样的映射上使用reflect.DeepEqual也可能返回false。

英文:

Disclaimer: Unrelated to map[string]int but related to testing the equivalence of maps in Go, which is the title of the question

If you have a map of a pointer type (like map[*string]int), then you do not want to use reflect.DeepEqual because it will return false.

Finally, if the key is a type that contains an unexported pointer, like time.Time, then reflect.DeepEqual on such a map can also return false.

答案6

得分: 5

使用 github.com/google/go-cmp/cmp 的 "Diff" 方法:

代码:

// 假设 got 是从某个测试逻辑中获得的值,want 是期望的黄金数据。
got, want := MakeGatewayInfo()

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

输出:

MakeGatewayInfo() 不匹配 (-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 个相同的元素
  		{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",
+ 		},
  	},
  }
英文:

Use the "Diff" method of 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 != &quot;&quot; {
    t.Errorf(&quot;MakeGatewayInfo() mismatch (-want +got):\n%s&quot;, diff)
}

Output:

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

答案7

得分: 1

最简单的方法:

	assert.InDeltaMapValues(t, got, want, 0.0, "单词计数错误。得到 %v,期望 %v", got, want)

示例:

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, "单词计数错误。得到 %v,期望 %v", got, want)
}
英文:

Simplest way:

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

Example:

import (
    &quot;github.com/stretchr/testify/assert&quot;
    &quot;testing&quot;
)

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

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

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

huangapple
  • 本文由 发表于 2013年8月13日 19:52:19
  • 转载请务必保留本文链接:https://go.coder-hub.com/18208394.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定