如何在 Golang 中测试两个 map 是否相等?

130

我有一个类似于下面这样的表驱动测试用例:

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

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


4
不行:迭代一个映射的顺序不能保证可预测性(predictable):_"对于映射的迭代顺序没有指定并且不保证从一次迭代到下一次迭代是相同的。..."_ - zzzz
2
此外,对于特定大小的地图,Go 将有意随机化顺序。强烈建议不要依赖该顺序。 - Jeremy Wall
尝试比较地图是程序设计中的一个设计缺陷。 - Inanc Gumus
5
请注意,从 Go 1.12(2019年2月)开始,为了便于测试,地图现在按键排序顺序打印。请参见下面的答案 - VonC
7个回答

253
Go语言的库已经为你准备好了。按照以下步骤操作即可:
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.")
}

如果你查看reflect.DeepEqualMap情况的source code,你会发现它首先检查两个地图是否都为nil,然后检查它们是否具有相同的长度,最后检查它们是否具有相同的(键,值)对的集合。
由于reflect.DeepEqual采用接口类型,因此它适用于任何有效的地图(map[string]bool, map[struct{}]interface{},等等)。请注意,它还适用于非映射值,因此请小心,确保您传递给它的是两个地图。如果您传递给它两个整数,它将愉快地告诉您它们是否相等。

太棒了,这正是我在寻找的。我想正如jnml所说,在测试用例中性能并不重要。 - andras
如果你想将这个用于生产应用,我建议尽可能使用自定义编写的函数,但如果性能不是问题,这绝对可以胜任。 - joshlf
3
@andras 你也应该看看 gocheck。它的使用非常简单,只需 c.Assert(m1,DeepEquals,m2)。它的好处是,在测试失败时可以输出实际结果和预期结果。 - Luke
14
值得注意的是,DeepEqual也要求切片的顺序相等。 - Xeoncross
1
DeepEqual文档 - Xeoncross
显示剩余5条评论

24
什么是表驱动测试中比较两个映射的惯用方法?您可以使用项目go-test/deep来帮助。但是,自从Go 1.12(2019年2月)起,这应该更容易地实现了,因为它已经原生支持。请参见release notes
fmt.Sprint(map1) == fmt.Sprint(map2)

fmt

现在按键排序打印映射以便于测试

排序规则如下:

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

在打印映射时,之前像 NaN 这样的非反射键值会显示为 <nil>。从此版本开始,将打印正确的值。

来源:

CL添加了:(CL代表“更改列表”

为此,我们添加了一个在根目录下的包,internal/fmtsort,它实现了一种通用机制来对地图键进行排序,而不考虑它们的类型。

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

新包是内部的,因为我们确实不希望每个人都使用它来排序。 它很慢,不通用,只适用于可以成为映射键的类型子集。

同时还可以使用text/template包中已经有的这种机制的弱化版本。

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


1
抱歉我的无知,我是Go的新手,但这个新的fmt行为究竟如何帮助测试映射的等价性呢?您是建议比较字符串表示而不是使用DeepEqual吗? - sschuberth
@sschuberth DeepEqual仍然不错。(或者更好的是cmp.Equal)使用案例在https://twitter.com/mikesample/status/1084223662167711744中有更详细的说明,例如在原始问题https://github.com/golang/go/issues/21095中所述的*日志*差异。意思是:根据您测试的性质,可靠的差异可以帮助您。 - VonC
1
fmt.Sprint(map1) == fmt.Sprint(map2) 的意思是将 map1map2 转换成字符串并比较它们是否相等。 - 425nesp
@425nesp 谢谢。我已经相应地编辑了答案。 - VonC

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]float64 的实例。eq 仅适用于 map[string]int 映射。每次我想要比较新类型映射的实例时,我应该实现 eq 函数的一个版本吗? - andras
@andras:11行代码。我可以“复制粘贴”并在比询问这个更短的时间内专门处理它。虽然许多人会使用“反射”来完成相同的任务,但那样的性能要差得多。 - zzzz
1
这是否期望地图以相同的顺序呈现?而Go语言并不保证,请参见https://blog.golang.org/go-maps-in-action中的“迭代顺序”。 - nathj07
3
不行,因为我们只会遍历 a - Torsten Bronger
从go1.18+开始,您可以使其通用。例如:https://go.dev/play/p/0RtJT32O5wU - Eric

12

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

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

测试失败

如果您的预期输出中的"order"地图与您的函数返回的不一致时,它仍将失败。然而,cmp仍然能够指出不一致的位置。

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

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

“在测试中使用reflect.DeepEqual通常是不明智的,这就是我们开源http://github.com/google/go-cmp的原因。” - Joe Tsai


5
使用github.com/google/go-cmp/cmp的“Diff”方法:

代码:

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

输出:

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

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

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

最后,如果键是包含未导出指针的类型,例如time.Time,则在此类映射上使用reflect.DeepEqual 也可能返回false


1
最简单的方法:

    assert.InDeltaMapValues(t, got, want, 0.0, "Word count wrong. Got %v, want %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, "Word count wrong. Got %v, want %v", got, want)
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接