testify difference between assert.ObjectsAreEqual and assert.ObjectsAreEqualValues

I found an interesting gotcha for comparing two objects deeply in testify.

I have two public dashboards that I want to compare. I want to make sure their values are equal. Originally when I was approaching this problem, I had to go digging through the docs to find

assert.ObjectsAreEqual and assert.ObjectsAreEqualValues

The difference is subtle but important.

Circling back, we can see the source code for both methods https://github.com/stretchr/testify/blob/v1.7.2/assert/assertions.go#L58

assert.ObjectsAreEqual first uses https://pkg.go.dev/reflect#DeepEqual then converts the object into a byte array and compares them. "It's sort of a relaxed recurse == operation"

In order to dissect what was happening, I rewrote assert.ObjectsAreEqualValues and added my own comments as I traversed the source code. I also wrote test cases until I could figure out a scenario where ObjectsAreEqual would file and ObjectsAreEqualValues would pass.

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. Somewhat of an edge-case to be false. See https://pkg.go.dev/reflect#Value.IsValid
	// Check if expectedValue type is convertible to the actual type
	fmt.Println("valid:", expectedValue.IsValid())
	fmt.Println("convertible:", expectedValue.Type().ConvertibleTo(actualType))
	if expectedValue.IsValid() && expectedValue.Type().ConvertibleTo(actualType) {
		// convert expectValue to actualType then get the interface representation
		// of the value and compare it to actual(interface)
		interfacesAreEqual := reflect.DeepEqual(expectedValue.Convert(actualType).Interface(), actual)

		fmt.Println("interfaces equal:", interfacesAreEqual)
		return interfacesAreEqual
	}

	// can't convert objects to each other, they must not be equal
	return false
}

It took me a while to find a scenario that would only work for ObjectsAreEqualValues. It ended up being quite simple. Comparing an anonymous struct to a named struct. They would not be the same type, but their values are the same

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

Now, the last and final GOTCHA (4), which brings me back to the reason for writing this in the first place...

In my test scenario, I was trying to compare the value between to structs of type public dashboard.  As you can see I'm pulling my hair out trying different things.

Test output looks as follows (apologies for however this word-wraps) 

models.PublicDashboard{Uid:"pubdash-uid", DashboardUid:"mwtZ5TZ4k", OrgId:1, TimeSettings:(*simplejson.Json)(0x140009ac300), IsEnabled:true, AccessToken:"", Crea
tedBy:7, UpdatedBy:0, CreatedAt:time.Date(2022, time.August, 26, 8, 21, 7, 0, time.UTC), UpdatedAt:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)}
&models.PublicDashboard{Uid:"pubdash-uid", DashboardUid:"mwtZ5TZ4k", OrgId:1, TimeSettings:(*simplejson.Json)(0x140002530f0), IsEnabled:true, AccessToken:"", Cre
atedBy:7, UpdatedBy:0, CreatedAt:time.Date(2022, time.August, 26, 8, 21, 7, 0, time.UTC), UpdatedAt:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)}
<nil>
<nil>
--- FAIL: TestIntegrationGetPublicDashboardConfig (0.07s)
    --- FAIL: TestIntegrationGetPublicDashboardConfig/returns_along_with_public_dashboard_when_exists (0.00s)
        database_test.go:276: 
                Error Trace:    database_test.go:276
                Error:          Should be true
                Test:           TestIntegrationGetPublicDashboardConfig/returns_along_with_public_dashboard_when_exists
FAIL
FAIL    github.com/grafana/grafana/pkg/services/publicdashboards/database       0.413s
FAIL

[Process exited 1]

As you can see they look exactly the same except the simpleJson values for TimeSettings.. which is strange because they should be nil, but there's a sneaky & hiding at the edge of the screen. cmd.PublicDashboard is a concrete type, while PublicDashboard Is a pointer.

Tl;dr assert.ObjectsAreEqual

assert.ObjectsAreEqualValues

Neither of these can dereference a pointer!