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


86

我有一个像这样的表驱动测试用例:

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
犯错,没有:迭代地图的顺序不能保证预测的“迭代为了在地图未指定,并不能保证是从一个迭代到下一个相同的......。”
zzzz

2
此外,对于某些大小的地图,Go会故意将顺序随机化。强烈建议不要依赖该顺序。
杰里米·沃尔

尝试比较地图是程序中的设计缺陷。
伊南克·古姆斯

4
请注意,在go 1.12(2019年2月)中,现在按键排序顺序打印地图以简化测试。请参阅下面的答案
VonC

Answers:


165

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情况下,你会发现,如果两个地图它首先检查是零,那么检查之前,他们具有相同的长度最后检查,看看他们是否有相同的一组(键,值)对。

由于reflect.DeepEqual采用接口类型,因此可以在任何有效的地图(map[string]bool, map[struct{}]interface{},等)上使用。请注意,它也适用于非地图值,因此请注意,传递给它的实际上是两个地图。如果您传递两个整数,它将很高兴地告诉您它们是否相等。


太棒了,这正是我想要的。我猜就像jnml所说的那样,它的性能不高,但是谁在乎测试用例。
Andras

是的,如果您要在生产应用程序中使用此功能,则在可能的情况下,我肯定会使用自定义编写的函数,但这绝对可以解决性能问题。
joshlf

1
@andras您还应该签出gocheck。一样简单c.Assert(m1, DeepEquals, m2)。这样做的好处是它会中止测试,并告诉您在输出中得到了什么以及期望什么。
路加福音

8
值得注意的是,DeepEqual还要求切片的ORDER相等
Xeoncross


13

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

您有go-test/deep需要帮助的项目。

但是:这应该是使用更加简单围棋1.12(2019年2月)本机:请参见发行说明

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

fmt

现在可以按键排序的顺序打印地图,以简化测试

排序规则是:

  • 适用时,nil比较低
  • 整数,浮点数和字符串排序依据 <
  • NaN比非NaN浮点数少
  • bool比较false之前true
  • 复杂比较真实,然后虚构
  • 指针按机器地址进行比较
  • 通道值按机器地址比较
  • 结构依次比较每个字段
  • 数组依次比较每个元素
  • 接口值首先通过reflect.Type描述具体类型进行比较,然后按照先前规则中的具体值进行比较。

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

资料来源:

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

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

这有点混乱并且可能很慢,但是地图的格式化打印从未如此快速,并且始终是反射驱动的。

新软件包是内部的,因为我们确实不希望每个人都使用此软件包对事物进行排序。它很慢,不通用,仅适用于可以作为映射键的类型的子集。

还可以使用中的软件包text/template,该软件包已经具有较弱的机制。

您可以看到在 src/fmt/print.go#printValue(): case reflect.Map:


对不起,很抱歉,我是Go的新手,但是这种新fmt行为究竟如何帮助测试地图的等效性?您是否建议比较字符串表示形式而不是使用DeepEqual
sschuberth

@sschuberthDeepEqual还是不错的。(或更确切地说cmp.Equal)用例在twitter.com/mikesample/status/1084223662167711744中得到了更多说明,如原始问题中所述的差异日志github.com/golang/go/issues/21095。含义:根据测试的性质,可靠的差异可以提供帮助。
VonC

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

@ 425nesp谢谢。我已经相应地编辑了答案。
VonC

11

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

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]float64eq仅适用于map[string]int地图。eq每当我想比较新型地图实例时,是否应该实现该函数的版本?
Andras

@andras:11个SLOC。我希望“复制粘贴”在比询问这个问题更短的时间内专门化。虽然,许多其他人会使用“反射”来执行相同的操作,但这会带来更差的性能。
zzzz

1
难道不希望地图的顺序相同吗?哪一步
nathj07 '16

3
@ nathj07不,因为我们仅通过进行迭代a
Torsten Bronger '16

5

免责声明map[string]int与在Go中测试地图的等效性无关,但与该问题的标题有关

如果您有地图指针类型的(像map[*string]int),那么你不会想用reflect.DeepEqual因为它会返回false。

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


2

使用github.com/google/go-cmp/cmp的差异”方法:

码:

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

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

1

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

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

测试失败

当预期输出中的映射“顺序”不是函数返回的内容时,它仍然会失败。但是,cmp仍然可以指出不一致之处。

供参考,我发现了这条推文:

https://twitter.com/francesc/status/885630175668346880?lang=zh-CN

“在测试中使用reflect.DeepEqual通常不是一个好主意,这就是为什么我们开放源代码的http://github.com/google/go-cmp”-Joe Tsai


-5

选项之一是修复rng:

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

请问,您的答案与这个问题有什么关系?
Dima Kozhevin

@DimaKozhevin golang在内部使用rng来混合映射中条目的顺序。如果修复了rng,您将获得可预测的订购顺序以进行测试。
Grozz

@Grozz是吗?为什么!?我不一定会争论它(我不知道),我只是不知道为什么会这样。
桑福德

我不在Golang上工作,所以我无法解释他们的推理,但是至少从v1.9开始,这种行为已得到确认。但是,我看到了一些类似的解释:“我们要强制您不能依赖地图中的顺序,因为您不应该这样。”
Grozz
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.