How to Check if Two Objects Look Like Each Other Without Using Equals

A colleague (from Design IT) and I were discussing a simple way to check two instances of an object. We wanted to know if all the public properties on one instance were holding the same values as the one on the other instance. We wanted to use this knowledge in a few unit tests to simply check all public fields on an instance in one fell swoop.

Since we didn't want this exact behavior at run-time we couldn't override Equals and check all object's properties, so we had to look for a different solution.

Consider an Address class for example, perhaps the one used in my series about N-Layer design. As you can imagine, an Address has properties like Street, HouseNumber, ZipCode and CountryCode. It could also have non-core properties like UpdateDateTime (a DateTime) and UpdatedBy (a Guid perhaps). An override of Equals could mark two instances of Address to be the same if  the properties Street, HouseNumber, ZipCode and CountryCode contain the same value on both instances.

However, in our unit tests, we also wanted to make sure that other properties like UpdateDateTime and UpdatedBy would contain the same values when we were dealing with two identical objects (content wise, not reference wise). Obviously, we could simply do something like this:

Assert.AreEqual<DateTime>(instanceA.UpdateDateTime, instanceB.UpdateDateTime);
Assert.AreEqual<Guid>(instanceA.UpdatedBy, instanceB.UpdatedBy);

But what happens when we would add a new property? For example, what happens if we add a CreateDateTime property to the Address class and forget to update the unit test? We'd probably miss this new property so we won't be able to tell if two instances contain the same value easily and fail the test if they aren't.

With a bit of reflection, it's reasonably easy to take two object instances and compare all their public properties. The following snippet shows a possible implementation of the LookLikeEachOther method on the new InstanceAssert class:

C#
public static void LookLikeEachOther(object a, object b)
{
  Type typeA = a.GetType();
  Type typeB = b.GetType();

  Assert.AreEqual(typeA, typeB, "The types of instances a and b are not the same.");

  PropertyInfo[] myProperties = typeA.GetProperties(BindingFlags.DeclaredOnly 
                      | BindingFlags.Public | BindingFlags.Instance);

  foreach (PropertyInfo myPropertyA in myProperties)
  {
    PropertyInfo myPropertyB = typeB.GetProperty(myPropertyA.Name);
    Assert.IsNotNull(myPropertyB, string.Format(@"The property {0} from instance a 
           was not found on instance b.", myPropertyA.Name));

    Assert.AreEqual<Type>(myPropertyA.PropertyType, myPropertyB.PropertyType, 
           string.Format(@"The type of property {0} on instance a is different from 
           the one on instance b.", myPropertyA.Name));

    Assert.AreEqual(myPropertyA.GetValue(a, null), myPropertyB.GetValue(b, null), 
           string.Format(@"The value of the property {0} on instance a is different from 
           the value on instance b.", myPropertyA.Name));
  }
}

VB.NET
Public Shared Sub LookLikeEachOther(ByVal a As Object, ByVal b As Object)
  Dim typeA As Type = a.[GetType]()
  Dim typeB As Type = b.[GetType]()

  Assert.AreEqual(typeA, typeB, "The types of instances a and b are not the same.")

  Dim myProperties As PropertyInfo() = typeA.GetProperties(BindingFlags.DeclaredOnly _
                    Or BindingFlags.[Public] Or BindingFlags.Instance)

  For Each myPropertyA As PropertyInfo In myProperties
    Dim myPropertyB As PropertyInfo = typeB.GetProperty(myPropertyA.Name)
    Assert.IsNotNull(myPropertyB, String.Format("The property {0} from instance a " & _
         "was not found on instance b.", myPropertyA.Name))

    Assert.AreEqual(myPropertyA.PropertyType, myPropertyB.PropertyType, _
         String.Format("The type of property {0} on instance a is different from " & _
         "the one on instance b.", myPropertyA.Name))

    Assert.AreEqual(myPropertyA.GetValue(a, Nothing), myPropertyB.GetValue(b, Nothing), _
         String.Format("The value of the property {0} on instance a is different from " & _
         "the value on instance b.", myPropertyA.Name))
  Next
End Sub

With this new method, checking two instances for content equality is now as easy as:

Address addressA = SomeMethodThatReturnsAnAddress("a");
Address addressB = SomeMethodThatReturnsAnAddress("a");
InstanceAssert.LookLikeEachOther(addressA, addressB);

If the properties of the two instances of addressA and addressB all contain the same values, this code runs successfully and doesn't assert. If the instances are not the same, or one of the properties contains a different value the LookLikeEachOther method will assert where necessary, causing the test to fail.

Does this make any sense? Does it look like a logical thing to do? If not, make yourself heard by using the Talk Back feature at the end of this article.

Extensions

One extension to this method could be an overload that accepts a string[] holding the names of the properties to ignore in the test. This could be useful if you have properties like a timestamp that you want to leave out of the compare operation

Unfortunately, since the Assert class is static, you cannot extend that class with an extension method like LookLikeEachOther. Therefore, I had to create a new static class called InstanceAssert. Do you know of any better solution to more tightly integrate the LookLikeEachOther method in the testing framework? In the project where we use this code we make use of the built-in unit testing features of Visual Studio 2008, but the same principle should apply to other frameworks like NUnit as well.


Where to Next?

Wonder where to go next? You can post a comment on this article.

Doc ID 442
Full URL https://imar.spaanjaars.com/442/how-to-check-if-two-objects-look-like-each-other-without-using-equals
Short cut https://imar.spaanjaars.com/442/
Written by Imar Spaanjaars
Date Posted 03/18/2008 22:27

Comments

Talk Back! Comment on Imar.Spaanjaars.Com

I am interested in what you have to say about this article. Feel free to post any comments, remarks or questions you may have about this article. The Talk Back feature is not meant for technical questions that are not directly related to this article. So, a post like "Hey, can you tell me how I can upload files to a MySQL database in PHP?" is likely to be removed. Also spam and unrealistic job offers will be deleted immediately.

When you post a comment, you have to provide your name and the comment. Your e-mail address is optional and you only need to provide it if you want me to contact you. It will not be displayed along with your comment. I got sick and tired of the comment spam I was receiving, so I have protected this page with a simple calculation exercise. This means that if you want to leave a comment, you'll need to complete the calculation before you hit the Post Comment button.

If you want to object to a comment made by another visitor, be sure to contact me and I'll look into it ASAP. Don't forget to mention the page link, or the Doc ID of the document.

(Plain text only; no HTML or code that looks like HTML or XML. In other words, don't use < and >. Also no links allowed.