【问题标题】:How to test the equivalence of maps in Golang?如何在 Golang 中测试 map 的等价性?
【发布时间】:2013-08-13 11:52:19
【问题描述】:

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

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

这假设等效映射的字符串表示是相同的,在这种情况下似乎是正确的(如果键相同,则它们散列到相同的值,因此它们的顺序将相同)。有一个更好的方法吗?在表驱动测试中比较两个地图的惯用方法是什么?

【问题讨论】:

  • Err, no: 不保证迭代 map 的顺序是predictable: "map 上的迭代顺序没有指定,不保证从一次迭代到下一个……”.
  • 此外,对于某些尺寸的地图,Go 会故意随机化顺序。强烈建议不要依赖该顺序。
  • 尝试比较地图是您程序中的设计缺陷。
  • 请注意,在 go 1.12(2019 年 2 月)中,地图现在按键排序顺序打印以简化测试。见my answer below

标签: testing maps go equivalence table-driven


【解决方案1】:

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

如果您查看source codereflect.DeepEqualMap 案例,您会看到它首先检查两个映射是否为零,然后检查它们是否具有相同的长度,然后最后检查如果它们具有相同的 (key, value) 对。

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

【讨论】:

  • 太棒了,这正是我想要的。我想 jnml 说它的性能不高,但谁在乎测试用例。
  • 是的,如果您希望将它用于生产应用程序,如果可能的话,我肯定会使用自定义编写的函数,但如果性能不是问题的话,这绝对可以解决问题。跨度>
  • @andras 您还应该查看gocheck。就像c.Assert(m1, DeepEquals, m2) 一样简单。这样做的好处是它会中止测试并告诉你你得到了什么以及你对输出的期望。
  • 值得注意的是,DeepEqual 还有requires the ORDER of slices to be equal
【解决方案2】:

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

您有项目 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 stands for "Change List")

为此,我们添加了a package at the root, internal/fmtsort,它实现了一种通用机制,用于对映射键进行排序,无论其类型如何。

这有点混乱,可能很慢,但地图的格式化打印从来没有这么快,而且已经一直是反射驱动的。

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

也可以使用text/template 中的包,它已经有这个机制的弱版本。

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

【讨论】:

  • 抱歉我的无知,我是 Go 新手,但是这种新的 fmt 行为究竟如何帮助测试地图的等效性?您是否建议比较字符串表示而不是使用DeepEqual
  • @sschuberth DeepEqual 还是不错的。 (或rather cmp.Equal)用例在twitter.com/mikesample/status/1084223662167711744 中有更多说明,如原始问题中所述的区分日志github.com/golang/go/issues/21095。含义:根据测试的性质,可靠的差异会有所帮助。
  • fmt.Sprint(map1) == fmt.Sprint(map2) tl;dr
  • @425nesp 谢谢。我已经相应地编辑了答案。
【解决方案3】:

改用 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=en

“在测试中使用 reflect.DeepEqual 通常是个坏主意,这就是我们开源 http://github.com/google/go-cmp 的原因” - 乔蔡

【讨论】:

    【解决方案4】:

    最简单的方法:

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

    【讨论】:

      【解决方案5】:

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

      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:11 个 SLOC。我会在比询问这个问题更短的时间内“复制粘贴”专门化它。虽然,许多其他人会使用“反射”来做同样的事情,但这样的性能要差得多。
      • 难道不希望地图的顺序相同吗?哪个 go 不能保证看到 blog.golang.org/go-maps-in-action上的“迭代顺序”@
      • @nathj07 不,因为我们只遍历a
      【解决方案6】:

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

      【讨论】:

        【解决方案7】:

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

        如果你有一个指针类型的映射(比如map[*string]int),那么你do not want to use reflect.DeepEqual 因为它会返回false。

        最后,如果 key 是一个包含未导出指针的类型,比如 time.Time,那么在这样的映射 can also return false 上使用 reflect.DeepEqual。

        【讨论】:

          猜你喜欢
          • 2021-01-02
          • 1970-01-01
          • 2013-10-03
          • 2011-12-15
          • 2022-04-27
          • 2017-03-29
          • 2011-09-03
          • 1970-01-01
          • 2021-01-03
          相关资源
          最近更新 更多