Dennis van der Veeke
Dennis van der Veeke

Reputation: 854

Compare structs that have slice fields ignoring item order with stretchr/testify

I have a problem where I need to compare two very large structs (protobuf generated) with each other, as part of a test-case. These structs have multiple nested arrays in them. Below is a simplified example that reproduces / demonstrates the problem.

package pkg

import (
    "github.com/stretchr/testify/assert"
    "reflect"
    "testing"
)

type structOne struct {
    Foo  string
    Offs []*structTwo
}

type structTwo struct {
    Identifier string
}

func Test_Compare(t *testing.T) {
    exp := &structOne{
        Foo: "bar",
        Offs: []*structTwo{
            {
                Identifier: "one",
            },
            {
                Identifier: "two",
            },
            {
                Identifier: "three",
            },
            {
                Identifier: "four",
            },
        },
    }

    act := &structOne{
        Foo: "bar",
        Offs: []*structTwo{
            {
                Identifier: "four",
            },
            {
                Identifier: "three",
            },
            {
                Identifier: "two",
            },
            {
                Identifier: "one",
            },
        },
    }

    assert.Equal(t, exp, act)                   // fails
    assert.True(t, reflect.DeepEqual(exp, act)) // fails
}

I have tried using assert.Equal(t, exp, act) and assert.True(t, reflect.DeepEqual(exp, act)). I am looking for a way to compare such structs, preferably without having to create custom comparison functions for all of objects.

Thank you

Upvotes: 9

Views: 9739

Answers (2)

Rafet
Rafet

Reputation: 926

You can do this by using IgnoreFields function of the cmpopts package. You'd need to specify which fields from which structs would need to be ignored.

The name may be a dot-delimited string (e.g., "Foo.Bar") to ignore a specific sub-field that is embedded or nested within the parent struct.

Here's an example:

got, want := SomeFunc()

if !cmp.Equal(want, got, cmpopts.IgnoreFields(Struct1{}, "Field1", "Field2", "Struct2.FieldA")) {
    t.Errorf("SomeFunc() mismatch")
}

Upvotes: 0

blackgreen
blackgreen

Reputation: 44656

You can use assert.ElementsMatch to compare two slices irrespective of element ordering.

ElementsMatch asserts that the specified listA(array, slice...) is equal to specified listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, the number of appearances of each of them in both lists should match.

However this applies only to the slice field itself. If your struct model has few fields, you can compare them one by one and use ElementsMatch on the slice:

    assert.Equal(t, exp.Foo, act.Foo)
    assert.ElementsMatch(t, exp.Offs, act.Offs)

If your structs have a lot of fields, you can reassign the slice values to temp variables, nil the fields out, and then compare:

    expOffs := exp.Offs
    actOffs := act.Offs

    exp.Offs = nil
    act.Offs = nil

    assert.Equal(t, exp, act) // comparing full structs without Offs
    assert.ElementsMatch(t, expOffs, actOffs) // comparing Offs separately

It would be even better if stretchr/testify allowed registering custom comparators for user-defined types, or checking if the objects implement a certain interface and call that to test equality

if cmp, ok := listA.(Comparator); ok { 
    cmp.Compare(listB) 
}

but I'm not aware of such a feature.


In alternative, https://github.com/r3labs/diff was suggested, which you can use as such. The order or slice items is ignored by default.

    // import "github.com/r3labs/diff/v2"
    changelog, err := diff.Diff(exp, act)
    assert.NoError(t, err)
    assert.Len(t, changelog, 0)

Upvotes: 10

Related Questions