How to Compare Object Instances in your Unit Tests Quickly and Easily
When unit testing, you may need to compare attribute equality instead of the default reference equality of two object instances. It might not be feasible to manually compare EVERY field with expected values in another object.
Here’s xUnit’s Assert.Equal<T>(T expected, T actual)
method:
/// <summary>
/// Verifies that two objects are equal, using a default comparer.
/// </summary>
/// <typeparam name="T">The type of the objects to be compared</typeparam>
/// <param name="expected">The expected value</param>
/// <param name="actual">The value to be compared against</param>
/// <exception cref="EqualException">Thrown when the objects are not equal</exception>
public static void Equal<T>(T expected, T actual)
{
Equal(expected, actual, GetEqualityComparer<T>());
}
Since the default comparer for object
is usually a referential comparison, we will not be able to use Assert.Equal<T>()
to test whether our actual object’s attributes are equal to our expected object’s attributes.
We could override the default equality comparer for our class… however, this is a test code smell called Equality Pollution, falling under a larger umbrella (cloud - if you will…) called Test Logic in Production. From the name, it’s pretty clear why it’s a smell:
The code that is put into production contains logic that should be exercised only during tests…
A system that behaves one way in the test lab and an entirely different way in production is a recipe for disaster!
So what can we do to compare the attribute equality of objects without overriding the default equality operator of the class under test?…
Anyone Else Done This?… Beuller…
Yep, there are a couple options:
- Pull in a third party extension to our test framework
- Write a custom equality assertion method in a separate test-specific class or subclass of the system under test
This is an example of an Expected State Verification test I wrote:
public void Assert_that_data_written_to_erp_does_not_change() {
List<ErpPayload> erpPayloads = new List<ErpPayload>();
// Mock web service and record arguments
mockErpPackageWriter.Setup(
x => x.WritePack(It.IsAny<UniFile>(),
It.IsAny<string>(),
It.IsAny<int[]>(),
It.IsAny<UniDynArray>()))
.Callback(
(UniFile file,
string recordId,
int[] fields,
UniDynArray arr) =>
erpPayloads.Add(new ErpPayload() {
FileName = file.FileName,
RecordId = recordId,
Fields = fields,
Arr = arr.StringValue
}));
var erpUtilities = new ERPUtilities(mockErpPackageWriter.Object);
var samplePart =
JsonConvert.DeserializeObject<Part>(
File.ReadAllText(@"..\..\Fixtures\part_sample.json"));
// Convert part and send it to mock web service
erpUtilities.UpdateErp(samplePart);
erpUtilities.Disconnect();
var deserializedErpPayloads =
JsonConvert.DeserializeObject<List<ErpPayload>>(
File.ReadAllText(@"..\..\Fixtures\erp_values_payloads.json"));
// Validate web service arguments didn't change
erpPayloads.ShouldBeEquivalentTo(deserializedErpPayloads);
}
This was a legacy application; I had to mock a web service to make sure arguments I was sending to it didn’t change. I needed to compare actual to expected instances of an entity with a very large graph. Here’s one instance…
For this regression test, it wasn’t not feasible to manually assert the attribute equality of two instances of this entity with:
...
Assert.Equal<Part>(instance1.partNumber, instance2.partNumber)
Assert.Equal<Part>(instance1.partDescription, instance2.partDescription)
Assert.Equal<Part>(instance1.weight, instance2.weight)
Assert.Equal<Part>(instance1.price, instance2.price)
...
Option 1: Third Party Extension
Notice my use of .ShouldBeEquivalentTo()
in my test assertion. It comes from Fluent Assertions, a “set of .NET extension methods that allow you to more naturally specify the expected outcome of a TDD or BDD-style test”. It’s always the second NuGet package I add after xUnit.net.
Here is the definition of the .ShouldBeEquivalentTo()
:
/// <summary>
/// Asserts that an object is equivalent to another object.
/// </summary>
/// <remarks>
/// Objects are equivalent when both object graphs have equally named properties with the same value,
/// irrespective of the type of those objects. Two properties are also equal if one type can be converted to another and the result is equal.
/// The type of a collection property is ignored as long as the collection implements <see cref="IEnumerable"/> and all
/// items in the collection are structurally equal.
...
public static void ShouldBeEquivalentTo<T>(this T subject, object expectation,
Func<EquivalencyAssertionOptions<T>, EquivalencyAssertionOptions<T>> config, string because = "",
params object[] reasonArgs)
{
...
}
You will usually have to set some options when using .ShouldBeEquivalentTo()
like:
Depth of recursion:
actual.ShouldBeEquivalentTo(expected, options => options.ExcludingNestedObjects());
Ignore members that don’t match:
actual.ShouldBeEquivalentTo(expected, options => options.ExcludingMissingMembers());
Fine-grained exclusions/inclusions:
actual.ShouldBeEquivalentTo(expected, options => options.Excluding(o => o.Customer.Name));`
More configuration options here on Github
Take a look at what else is offered by Fluent Assertions, it offers many more methods for making your test assertions more expressive.
Option 2: Roll Your Own Custom Equality Assertion
If you can’t (or don’t want to) rely on an external extension to your testing framework, you can also roll your own equality assertion method via System.Reflection
.
This is from a Stack Overflow answer here:
public static bool PublicInstancePropertiesEqual<T>(this T self, T to, params string[] ignore) where T : class
{
if (self != null && to != null)
{
var type = typeof(T);
var ignoreList = new List<string>(ignore);
var unequalProperties =
from pi in
stype.GetProperties(BindingFlags.Public | BindingFlags.Instance)
where !ignoreList.Contains(pi.Name)
let selfValue = type.GetProperty(pi.Name).GetValue(self, null)
let toValue = type.GetProperty(pi.Name).GetValue(to, null)
where selfValue != toValue &&
(selfValue == null || !selfValue.Equals(toValue))
select selfValue;
return !unequalProperties.Any();
}
return self == to;
}
This might require some customization to suit your needs, but if you don’t want to bloat your test project with external references, then this is definitely an option.
Summary
So we learned a few things here:
- Don’t override
Object.Equals
just for testing purposes, it’s a test code smell. - People have implemented attribute equality before, so look around for existing solutions.
- Depending on the depth of your object graph, you can either
- use
FluentAssertions.ShouldBeEquivalentTo()
- implement your own equality assertions (only within your test assembly, NOT your SUT).
- use
- With Fluent Assertions you get some nice configuration options and lots of additional assertion extension methods.
PS…
I’ve written a book that goes into detail about that tough situation of adding tests to a brownfield app. Click below to find out more!
Make sure you sign up for my newsletter to hear more testing tips and tricks!
Related Posts: