Wednesday, May 02, 2007

Test Utility Classes

When you start to write a substantial number of unit tests, you may discover that your tests have quite a bit in common. For instance, if you have a CreditCard class, you may have a block of code like this in each of your tests:

Assert.AreEqual( expectedCard.Number,     actualCard.Number );
Assert.AreEqual( expectedCard.FirstName, actualCard.FirstName );
Assert.AreEqual( expectedCard.MiddleName, actualCard.MiddleName );
Assert.AreEqual( expectedCard.LastName, actualCard.LastName );
Assert.AreEqual( expectedCard.ExpMonth, actualCard.ExpMonth );
Assert.AreEqual( expectedCard.ExpYear, actualCard.ExpYear );

Of course, it won't be exactly the same in each test, and that just amplifies the problem. What happens when you want to add a variable to CreditCard? Copy, paste, paste, paste... and inevitably, miss one. One way to manage this complexity is to implement a custom Equals() method in your class. With that in place, you can collapse the previous code into this:

Assert.AreEqual( expectedCard, actualCard );

Sure enough, this reduces the amount of code in your unit tests and makes them less error-prone. However, if the equality test fails, you will know that one of the credit card's values is incorrect, but you won't know which. You can probably figure that out with a little debugging, but I prefer tests to be more explicit when they fail. So to deal with this, I'll typically create a test utility class for each class that looks something like this:

public class CreditCardUtil
public static void AssertAreEqual( CreditCard expected, CreditCard actual )
Assert.AreEqual( expected.Number, actual.Number );
Assert.AreEqual( expected.FirstName, actual.FirstName );
Assert.AreEqual( expected.MiddleName, actual.MiddleName );
Assert.AreEqual( expected.LastName, actual.LastName );
Assert.AreEqual( expected.ExpMonth, actual.ExpMonth );
Assert.AreEqual( expected.ExpYear, actual.ExpYear );

This way, the unit test code is just as simple, but you get meaningful error messages when the test fails. This pattern is useful when you're working on a TDD project with a group, because each person on the team will know where to look if they need a particular bit of functionality. Looking for a method that will compare two Invoices? Try InvoiceUtil.AssertAreEqual. Not there? Write it.

You may want to extend these utility classes to do other useful things, such as:
  • Instance an object that is initialized with unique properties (useful for comparison)
  • Perform CRUD database operations
  • Setup all of the object's dependencies
  • Define useful constants, file paths, etc. specific to the object
This may seem obvious, but I've seen a number of people (myself included) treat test code as a second class citizen in their projects. The beginner's approach is typically to:
  1. Find a similar test
  2. Copy it
  3. Change it a little
  4. Repeat
This is fine when the code is well-factored, but in the beginning, it usually isn't. Take the time to tighten up your test code before it's too late. That way, when your manager wants to add CVV numbers to credit card processing, you'll be in a much more responsive position. Remember, test code is production code.

No comments: