TL;DR: Use version 6+ of FluentAssertions when asserting equality of record types with collections in them.
I already mentioned the possible unexpected behavior of ToString
in base class in another post.
This time I write about unexpected behavior of records when used with common libraries.
An important library I use for writing assertions in tests is FluentAssertions. One of its most important features is its built-in structural equality check in the BeEquivalentTo
assertion method. The method saves you from implementing Equals
on types just because you need structural equality checking in your unit tests. You can even compare any instance to any other instance independent of type. If they are structurally equal, BeEquivalentTo
will let the assertion pass.
Now, guess what happens if we try BeEquivalentTo
on record types? Will it just work because of their compiler-synthesized Equals
method? Will it just work because of FluentAssertions’ structural equality mechanism?
With some records, everything seems to be fine, like in this example:
record Point(int X, int Y); public void SomeTest() { Point point1 = new(4, 2); Point point2 = new(4, 2); // passes point1.Should().BeEquivalentTo(point2); }
But with other records, behavior maybe unexpected:
record Point(int X, int Y); record PointCollection(IEnumerable<Point> Points); public void SomeTest() { Point point1 = new(1, 2); Point point2 = new(3, 4); PointCollection pointCollection1 = new(new[] { point1, point2 }); PointCollection pointCollection2 = new(new[] { point1, point2 }); // fails pointCollection1.Should().BeEquivalentTo(pointCollection2); }
What happened under the hood to make this test fail?
Because record types are reference types, FluentAssertions checks whether they override the Equals
methods, which they do because of the compiler-synthesized methods. Because they override Equals
, FluentAssertions will not use its own structural equality check, but the record type’s Equals
method.
This does not make a difference in the first example and not even when we nest records. But as soon as we use standard collection types in our records, it will break our tests. None of the collection types in the .NET library — not even ImmutableArray<T> — implements its Equals
method by element-wise comparison.
You now have a couple of options to save your tests.
The simplest one is to switch to (at the time of writing) the latest preview version of FluentAssertions 6, which has different default behavior for record types (see discussion and PR).
If you cannot do that for some reason, you can force FluentAssertions to use member-wise comparison with ShouldBeEquivalentTo(myRecord, opts => opts.CompareByMember<MyRecord>())
.
This becomes verbose and annoying really fast. Even creating a new convenience extension method like ShouldBeMemberwiseEquivalentTo<T>
is not of much help. Because as soon as you have nested records like in the second example above, you have to tell FluentAssertions to use member-wise equality check for every! single! type! in your data structure, like this
pointCollection1.Should().BeEquivalentTo( pointCollection2, opts => opts.CompareByMember<PointCollection>().CompareByMember<Point>())
If you can help it, use FluentAssertions 6 and it will Just Work.