Interesting behavior comparing structs in testify
I needed to compare two structs in a test to make sure their values were equal. Digging through testify’s docs, I found two methods that seemed relevant: assert.ObjectsAreEqual and assert.ObjectsAreEqualValues. The difference between them wasn’t immediately clear, and I ran into some surprising behavior along the way.
TL;DR: Use
assert.ObjectsAreEqualwhen you want types to be the same andassert.ObjectsAreEqualValueswhen you want the values to be the same but the types can differ. Neither can dereference a pointer.
The first surprise
These methods don’t actually assert anything — they return a boolean. The names are misleading, and this has been documented as an issue. You need to wrap them:
assert.True(t, assert.ObjectsAreEqual(obj1, obj2))
Digging into the source
Looking at the source code, assert.ObjectsAreEqual uses reflect.DeepEqual and then converts objects to byte arrays for comparison. It’s a relaxed recursive == operation — but it requires arrays to be in the same order. Be careful when comparing objects with arrays, maps, or interfaces.
To understand ObjectsAreEqualValues, I rewrote it with my own comments:
func MyObjectsAreEqualValuesFunc(expected, actual interface{}) bool {
fmt.Println("----- next scenario ------")
if assert.ObjectsAreEqual(expected, actual) {
fmt.Println("assert.ObjectsAreEqual: true")
return true
}
// get the concrete type
actualType := reflect.TypeOf(actual)
// Check for nil type. At this point if both == nil deep equal
// would have returned. This catches the case were one is nil
// and the other is not.
if actualType == nil {
fmt.Println("No actual type!")
return false
}
// get the value of expected
expectedValue := reflect.ValueOf(expected)
// Check if expectedValue represents 0 value. Edge-case.
// See https://pkg.go.dev/reflect#Value.IsValid
// Also check if expectedValue type is convertible to actualType
fmt.Println("valid:", expectedValue.IsValid())
fmt.Println("convertible:", expectedValue.Type().ConvertibleTo(actualType))
if expectedValue.IsValid() && expectedValue.Type().ConvertibleTo(actualType) {
// Convert expectValue to actualType, get the interface
// representation, and compare it to actual(interface)
converted := expectedValue.Convert(actualType).Interface()
interfacesAreEqual := reflect.DeepEqual(converted, actual)
fmt.Println("interfaces equal:", interfacesAreEqual)
return interfacesAreEqual
}
// can't convert objects to each other, they must not be equal
return false
}
When does it matter?
I wrote test cases until I found a scenario where ObjectsAreEqual fails but ObjectsAreEqualValues passes. It’s simple: compare a named struct to an anonymous struct with the same fields.
type Person struct {
Name string
}
a := Person{Name: "Jeff"}
b := struct {
Name string `json:"a.name"`
}{Name: "Jeff"}
assert.True(t, MyObjectsAreEqualValuesFunc(a, b))
assert.True(t, assert.ObjectsAreEqualValues(a, b))
The types are different, but the values are the same — exactly what ObjectsAreEqualValues is for.
Summary
Use assert.ObjectsAreEqual when you want types to be the same and assert.ObjectsAreEqualValues when you want the values to be the same but the types can differ. Neither can dereference a pointer, so if you’re comparing pointers, you’ll need to dereference them yourself first. Both methods return booleans instead of asserting, so wrap them in assert.True or you won’t catch failures.