Search code examples
dictionarygomatchfuzzy

How do I implement Python's mock ANY in Golang


I want to be able to compare two dictionaries while ignoring the value of some fields. This is extremely easy in Python with mock.ANY.

from unittest.mock import ANY
actual = {'userName':'bob', 'lastModified':'2012-01-01'}
expected = {'userName':'bob', 'lastModified': ANY}
assert actual == expected

Here's my attempt in Go that leverages cmp which allows you to implement a Equal method (https://pkg.go.dev/github.com/google/go-cmp/cmp#Equal) but it doesn't work. I believe it's because my MatchAll struct is not "assignable" to string. Is there anyway to get around this?

package main

import (
    "fmt"

    "github.com/google/go-cmp/cmp"
)

type MatchAll string

func (m MatchAll) Equal(v string) bool {
    return true
}

func main() { 
    matchAll := MatchAll("bruh")
    actual := map[string]any{
        "key": matchAll,
    }

    expected := map[string]any{
        "key": "foobar",
    }

    fmt.Println(cmp.Equal(expected, actual))
}

Solution

  • Looking at the cmp code, I believe the problem here is that when comparing map elements, the type that's used to check for the existence of the Equal method is the element type of the map (here, any), not the concrete type of one of the two values (here, string or MatchAll). Since the any type does not declare an Equal method, it falls back to the built-in comparisons.

    As an alternative, you could potentially use the cmp.FilterValues and cmp.Comparer functions to construct an option that supports your special "equal-to-anything" value. For example, this would appear to work as intended:

    package main
    
    import (
        "fmt"
    
        "github.com/google/go-cmp/cmp"
    )
    
    type matchAllType struct{}
    
    var matchAll = matchAllType{}
    
    var matchAllOption = cmp.FilterValues(
        // Filter function: returns `true` only for values using this comparison.
        func(x, y any) bool {
            return x == matchAll || y == matchAll
        },
        // Comparison function: called for pairs of values the filter accepts.
        cmp.Comparer(func(x, y any) bool { return true }),
    )
    
    func main() {
        actual := map[string]any{
            "key": matchAll,
        }
    
        expected := map[string]any{
            "key": "foobar",
        }
    
        fmt.Println(cmp.Equal(expected, actual, matchAllOption))
    }
    

    The always-true comparison function could possibly be replaced with cmp.Ignore() as well, though I'm not entirely sure whether the semantics are exactly the same. It does work in the above example, at least.

    There is also the cmpopts.IgnoreMapEntries option, which could be used as follows, but with slightly different semantics:

    var matchAllOption = cmpopts.IgnoreMapEntries(func(_, v any) bool { return v == matchAll })
    

    The difference is that with this option, the comparison would be true even if the actual dictionary did not contain an entry with a matching key at all, because the built-in map comparison tests whether "recursively calling Equal on all non-ignored map entries report equal", emphasis added. By contrast, the earlier solutions still require the other map to contain some value with the same key.