testify icon indicating copy to clipboard operation
testify copied to clipboard

Custom assert equal comparator interface

Open Southclaws opened this issue 2 years ago • 9 comments

Is there a way, or a v2 plan to provide more user friendly ways of diffing values for large structs?

Two types I make use of a lot are times and money-friendly decimals, and this combined with needing to compare large structs results in...

image

image

Which is unreadable.

For simple cases I compare the .String() output like a.Equal(want.String(), got.String()) which is a little more typing but it's fine for single values.

The problem is when asserting structs that have many fields. I don't want to waste time pulling out all these fields into tons of assertions just so I can add .string() to each item.

Ideally, I'd like to satisfy some comparator interface for our custom money type so diffs and comparisons are done against the rendered value not the underlying data. One other detail of decimal values is different combinations of bases and exponents can result in the same actual number. Assertions fail on this even if the actual resulting number is the same.


type MonetaryValue struct {
	Num decimal.Decimal `json:"num"` // The underlying value: 1234.69
	Cur BaseCurrency    `json:"cur"` // the currency identifier: USD
	Fmt string          `json:"fmt"` // the formatted value: $1,234.69
}

func (v MonetaryValue) GoString() string {
	return fmt.Sprintf(`"%s" (%s, %f)`, v.Fmt, v.Cur, v.Num.InexactFloat64())
}

func (v MonetaryValue) String() string {
	return v.Fmt
}

I've already got this, which gets me slightly nicer diffs but the comparison is still conducted against the underlying struct data, which isn't helpful for this particular case.

What would be ideal is a simple test interface:

func (v MonetaryValue) TestCompare(against MonetaryValue) bool {
  return v.Fmt == against.Fmt
}

Or maybe a way to surface better diff strings too.

go-cmp kind of solves this but it's not across all tests via an interface, it's just for a single assert - which is cumbersome to add to every test when you have hundreds.

gt.DeepEqual(t, want, got, cmp.Transformer("MonetaryValue", func(in currency.MonetaryValue) string {
		return in.Fmt
}))

Southclaws avatar Jun 17 '22 15:06 Southclaws

+1

Could be some kind of extension, similar to Hamcrest used in Java to implement Matchers. Something similar is already in place with mock.MatchedBy, but just for parameters evaluation/comparision.

matthiasgubler avatar Jul 02 '22 23:07 matthiasgubler

One of the other issues I forgot to mention here is the need to define equality.

The example above uses Shopspring's decimal library, which is a FPA decimal type. This type can represent the same number in a variety of ways, by moving the exponent along a specific value. 1234 with an exponent of -1 is the same as 12340 with an exponent of -2 for example. Semantically, these are the same number, but when diff'd, there's no way to teach the comparison code about this type of semantic equality, it only cares about the byte-for-byte equality.

Southclaws avatar Jul 07 '22 21:07 Southclaws

I feel diff for time.Time is also unreadable.

seiyab avatar Nov 04 '22 00:11 seiyab

Elsewhere, we extended DeepEqual with a couple of features. Via tags: https://github.com/weaveworks/scope/blob/477f6782f4622451723b2856bea49dca64052e53/test/reflect/deepequal.go#L146-L149

and to call a DeepEqual method if defined on the values: https://github.com/weaveworks/scope/blob/477f6782f4622451723b2856bea49dca64052e53/test/reflect/deepequal.go#L60-L66

bboreham avatar Mar 24 '23 19:03 bboreham

I'm building a personal finance app using decimal library and I've also found quite difficult to assert on it. Let me share a couple of tricks I've used:

  • When comparing decimal.Decimal, compare the String() value.
  • When comparing structs containing decimal.Decimal, serialise the structs to JSON and then assert.JsonEq() (limitation: this requires all struct fields to be exported)

pracucci avatar Jul 09 '23 06:07 pracucci

Up. Maybe we could register types with its comparison functions?

Something like:

testify.RegisterComparison[T any](t T, fn func(a,b T) bool )

and use it like:

testify.RegisterComparison[decimal.Decimal](t T, fn func(a,b decimal.Decimal) bool {
       return a.Equals(b)
})

lrweck avatar Sep 12 '23 16:09 lrweck

Wouldn't it be cleaner and simpler to have the library defer to func (t T) Equal(u T) bool if it exists.. time.Time and decimal.Decimal already have that method.

Like others we've resorted to workarounds like comparing things as JSON... this would be so much nicer!

rowanseymour avatar Feb 18 '24 18:02 rowanseymour

Wouldn't it be cleaner and simpler to have the library defer to func (t T) Equal(u T) bool if it exists.. time.Time and decimal.Decimal already have that method.

Like others we've resorted to workarounds like comparing things as JSON... this would be so much nicer!

Some Equal methods, such as time.Time.Equal from the standard library do not really mean the objects are equal, in that case it just means they represent the same instant. We can't defer to the Equal method without changing the definition of equal.

For this issue I would support approaches which make differences easier to understand.

brackendawson avatar Feb 18 '24 20:02 brackendawson