How to test the equivalence of maps in Golang?
Asked Answered
C

7

138

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?

Cirrhosis answered 13/8, 2013 at 11:52 Comment(4)
Err, no: The order iterating a map is not guaranteed to be predictable: "The iteration order over maps is not specified and is not guaranteed to be the same from one iteration to the next. ...".Psychopathy
Furthermore for maps of certain sizes Go will intentionally randomize the order. It's highly advisable not to depend on that order.Macleod
Trying to compare a map is a design flaw in your program.Yasmin
Note that with go 1.12 (Feb. 2019), Maps are now printed in key-sorted order to ease testing. See my answer belowShannan
C
263

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.

Coiffure answered 13/8, 2013 at 14:21 Comment(10)
Awesome, that's exactly what I was looking for. I guess as jnml was saying it's not as performant, but who cares in a test case.Cirrhosis
Yeah, if you ever want this for a production application, I'd definitely go with a custom-written function if possible, but this definitely does the trick if performance isn't a concern.Coiffure
@Cirrhosis You should also check out gocheck. As simple as c.Assert(m1, DeepEquals, m2). What's nice about this is it aborts the test and tells you what you got and what you expected in the output.Negative
It's worth noting that DeepEqual also requires the ORDER of slices to be equal.Harlanharland
Documentation for DeepEqual.Harlanharland
@Harlanharland thank you for the link, but it is better to add it to the answer.Ibbison
@VitalyZdanevich I did add it to the answer, it's just 270px lower than you wanted.Harlanharland
@Harlanharland I think it is better to always try to minimize the amount of comments - because they collapse after 10, and inserting inside the answer provides better context. For example this comment (my comment) is not an answer, so it is real comment.Ibbison
if map type is map[int64][]int64 then deep equals fails. Example: play.golang.org/p/2skYYgnAfl9 m1 := make(map[int64][]int64, 0) m2 := make(map[int64][]int64, 0) m1[1] = []int64{1, 2} m2[1] = []int64{2, 1} fmt.Println(reflect.DeepEqual(m1, m2)) // prints falseVibrato
@Vibrato Slices are only considered equal if they have the same elements in the same order. So {1, 2} and {2, 1} are not considered equal: play.golang.org/p/YWHqvROUtpYCoiffure
S
27

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 <
  • 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 <nil>. 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:

Shannan answered 13/1, 2019 at 21:18 Comment(4)
Sorry for my ignorance, I'm new to Go, but how exactly does this new fmt behavior help to test the equivalence of maps? Are you suggesting to compare the string representations instead of using DeepEqual?Believe
@Believe DeepEqual is still good. (or rather cmp.Equal) The use case is more illustrated in twitter.com/mikesample/status/1084223662167711744, like diffing logs as stated in the original issue: github.com/golang/go/issues/21095. Meaning: depending on the nature of your test, a reliable diff can help.Shannan
fmt.Sprint(map1) == fmt.Sprint(map2) for the tl;drRollway
@Rollway Thank you. I have edited the answer accordingly.Shannan
P
16

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
}
Psychopathy answered 13/8, 2013 at 11:59 Comment(5)
OK, but I have an other test case where I want to compare instances of map[string]float64. eq only works for map[string]int maps. Should I implement a version of the eq function every time I want to compare instances of a new type of map?Cirrhosis
@andras: 11 SLOCs. I'd "copy paste" specialize it in shorter time than it takes to ask about this. Though, many others would use "reflect" to do the same, but that's of much worse performance.Psychopathy
doesn't that expect the maps to be in the same order? Which go does not guarantee see "Iteration order" on blog.golang.org/go-maps-in-actionDoordie
@Doordie No, because we iterate only through a.Inhibitor
As of go1.18+, you may make it generic. e.g go.dev/play/p/0RtJT32O5wUAssistance
L
13

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

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

Failed test

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

Livvie answered 24/2, 2020 at 12:47 Comment(0)
M
6

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.

Matelda answered 28/3, 2017 at 21:34 Comment(0)
H
5

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 != "" {
    t.Errorf("MakeGatewayInfo() mismatch (-want +got):\n%s", diff)
}

Output:

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",
+       },
    },
  }
Hatshepsut answered 23/4, 2019 at 9:30 Comment(0)
M
1

Simplest way:

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

Example:

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)
}
Marquee answered 11/9, 2019 at 11:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.