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 read existing comments below or you can post a comment yourself on this article .


Consider making a donation
Please consider making a donation using PayPal. Your donation helps me to pay the bills so I can keep running Imar.Spaanjaars.Com, providing fresh content as often as possible.



Feedback by Other Visitors of Imar.Spaanjaars.Com

On Wednesday, March 19, 2008 7:09:12 PM Amit Kumar said:
I think there is a better option -

Above method is also using the same property and type name. We can Serialize both objects in xml and use simple string comparison.
On Wednesday, March 19, 2008 7:12:51 PM Imar Spaanjaars said:
Hi Amit,

While that would certainly work, it wouldn't give you the benefit of telling exactly what properties are different, or whether the underling types are different, right?

Imar
On Monday, March 31, 2008 7:24:23 PM Nisar said:
@Amit Kumar:
whould you mind showing?
thanks
On Friday, January 13, 2012 4:04:02 PM Chris Cline said:
Great method. This will help immensely with Unit Testing.

One suggestion for modification: Add another loop to find properties in B that are not in A.
On Friday, January 13, 2012 7:08:58 PM Imar Spaanjaars said:
Hi Chris,

Yes, good suggestion....

Cheers,

Imar
On Thursday, January 31, 2013 2:05:35 PM Dean said:
Hi

This was very helpful thanks but DateTime types fail the AreEqual.  DateTime is compared at the tick level and when the values being compared are from different sources EG SQL DateTime and C# DateTime the ticks are different.

Within the Foreach I did this -
if (myPropertyA.PropertyType.FullName == "System.DateTime")
                {
                    DateTime expectedDateTime = (DateTime)myPropertyA.GetValue(a, null);
                    DateTime actualDateTime = (DateTime)myPropertyA.GetValue(b, null);
                    Assert.AreEqual(expectedDateTime.ToString(), actualDateTime.ToString(), string.Format(@"The value of the property {0} on instance a is different from the value on instance b.", myPropertyA.Name));
                }

Hope that helps someone.
On Friday, February 01, 2013 4:33:31 PM Imar Spaanjaars said:
Hi Dean,

Thanks for that. An alternative is to use each date's ticks and then use an overload of AreEqual that enables you to specify a delta for the allowed inaccuracy:

Assert.AreEqual(expectedDateTime, actualDateTime.Ticks, 100);

Cheers,

Imar


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 QuickDocId of the document.

For more information about the Talk Back feature, check out this news item.