Part 3: Automating Unit Testing with a Base Class

In Part 1 of Automating Unit Testing with a Base Class I provided a brief introduction to Unit Testing, provided an overview of the problems that unit testing business objects present, and briefly discussed why I include the database in my unit tests.  In part 2, I provided an overview of the process I've followed in testing the CRUD operations of my business objects.  In this final installment I'm going to discuss how I've simplified the testing of basic CRUD operations on my business objects by creating a base class for my business object unit tests.

Let's Review

Before moving on to the solution I'd like to quickly review the process I follow in testing the CRUD operations on my business objects.

Create

  •  Instantiate an instance of the object
  • Set the properties of the object with valid values.
  • Call the Save() method on the object, checking that the save succeeds.

Read

  • Do Create.
  • Retrieve the object out of the data store and check that the values of the saved object equal the values pulled from the data store.

Update

  • Do Read
  • Change the properties of the object.
  • Call the Save() method on the object.
  • Retrieve the object out of the data store and check that the values of the updated object equal the values pulled from the data store.

Delete

  • Do Create
  • Call Delete() on the object.
  • Try reading the object back out of the data store and ensure we get a null object (since its deleted).

See Part 2 for the code representation of the above

What can we Automate?

Over the past year I've written a lot of unit tests for the CRUD operations on my business objects.  A couple months ago I was working on a moderately sized .NET project coding away, writing failing tests for each of my CRUD operations, writing the code to make the tests past, and refactoring my way to cleaner code.  The process was feeling pretty good, except for one thing.  I seemed to be writing the same code over and over.  Duplication is bad, so why do I allow myself to write 50 test classes with almost identical logic for testing the CRUD operations on my objects?  I try to follow the Don't Repeat Yourself principle that Dave Thomas and Andy Hunt present in The Pragmatic Programmer so I set out to find a better way.

As I began my journey I began to evaluate the tests that I was writing for my business objects.  The tests were slightly different across the project but I saw a similarity among them that led me down an interesting path.  Each test object was essentially performing 3 different tasks, the only difference between the tests was the objects that they were performing the tasks on.

  • Task 1 - Loading objects with data.
  • Task 2 - Calling methods on my business objects (Save, Delete, Load).
  • Task 3 - Comparing two business objects to ensure the values saved and the values loaded were identical.

Task 1 - Loading Objects with data

The first task I identified was the process of loading objects with data.  In order to test the CRUD operations on my objects I first needed to load the objects with data so they could be saved to the database.  Let's take a quick look at how the data was loaded in the unit tests outlined in Part 2 of this series:

Customer customer = new Customer();
customer.ContactName = "Steve Eichert";
customer.Address = "221 South North West Ave.";
customer.City = "Philadelphia";
customer.Region = "PA";

To load the Customer object with data I instantiate an instance of the Customer class and then set the properties of the customer.  The class being instantiated and the properties being set change for each unit test, however, the process is the same.  First instantiate an instance of the class and then set the properties of that class.  Now that the task has been identified how can it be automated?

Provided we have the type of the business object we can use Activator.CreateInstance to instantiate an instance of the class.

BusinessObject bizObject = (BusinessObject) Activator.CreateInstance(BusinessObjectType);

After an instance of the class is created we need a method for setting the properties of the object.  The System.Reflection namespace gives us just what we need.  By using the GetProperties() method on the System.Type object we can identify all the properties defined on our objects.

///

<summary>
/// Load the properties of an BusinessObject with random values.
/// </summary>
/// <param name="bizObject">The <c>BusinessObject</c> to load the properties of.</param>
public
void
LoadProperties(BusinessObject bizObject) {
   System.Reflection.PropertyInfo[] properties = BusinessObjectType.GetProperties();
    foreach(PropertyInfo property in properties) {
      if(property.CanWrite) {
      
SetDynamicPropertyValue(bizObject, property);
     }
   }
}

The LoadProperties() method retrieves all the properties available on our business object and then loops over all the properties and sets appropriate values (SetDynamicPropertyValue).  Depending on the type (string, int, bool, etc) of the property we need to set a different value.  We also need to consider that certain properties have a limited set of potential values.  To set the value of each property we need to first determine the type of property using the PropertyType property of the PropertyInfo object (say that 10 times fast).  The below block of code shows how this can be accomplished.

///

<summary>
/// Set the property of an <c>BusinessObject</c> to a random (dynamic) value.
/// </summary>
/// <param name="bizObject">The <c>BusinessObject</c> to set the value of.</param>
/// <param name="property">The <c>PropertyInfo</c> to set the value of.</param>
private void SetDynamicPropertyValue(BusinessObject bizObject, PropertyInfo property) {
  Random random;
  string propertyKey = bizObject.GetType().Name + "-" + property.Name;
  if(_allowableValues[propertyKey] != null) {
    
object[] values = (object[]) _allowableValues[propertyKey];
    random =
new Random(Environment.TickCount);
    property.SetValue(bizObject, values.GetValue(random.Next(values.Length)),
null);
  }
else {
    ArrayList usedValues = (ArrayList)_uniqueValues[propertyKey];
    
switch(property.PropertyType.ToString()) {
      
case "System.String":
          property.SetValue(bizObject, GetRandomString(usedValues),
null);
         
break;}
      
case "System.Int32":
      
case "System.Double":
          random =
new Random(Environment.TickCount);
          property.SetValue(bizObject, random.Next(1, 9999),
null);
          
break;
       
case "System.Decimal":
          random =
new Random(Environment.TickCount);
          property.SetValue(bizObject, Convert.ToDecimal(random.Next(1, 9999)),
null);
         
break;
      
case "System.DateTime":
          Random dayRandom =
new Random(Environment.TickCount);
          Random monthRandom =
new Random(Environment.TickCount);
          property.SetValue(bizObject, DateTime.Parse(monthRandom.Next(1, 12) + "/" +
                                                                          dayRandom.Next(1, 2 + "/" +
                                                                          DateTime.Now.Year),
null);
         
break;
      
case "System.Boolean":
          random =
new Random(Environment.TickCount);
          property.SetValue(bizObject, Convert.ToBoolean(random.Next(0, 1)),
null);
         
break;
      
default:
         
if(property.PropertyType.BaseType != null && property.PropertyType.BaseType.ToString() == "System.Enum") {
               Array values = Enum.GetValues(property.PropertyType);
               random =
new Random(Environment.TickCount);
               property.SetValue(bizObject, values.GetValue(random.Next(values.Length)),
null);
          }
         
break;
       }
   }
}

The SetDynamicPropertyValue() method accepts the business object being tested (bizObject) as well as the current property that we need to assign a value to (property).  The method evaluates the type of property using the "PropertyType" property of the PropertyInfo object.  There's a couple utility methods that I'm not going to discuss at the moment as to stay on topic.  The bottom line is that by inspecting the PropertyType attribute of each property that we retrieve from the business object we can determine a valid value to assign to the property.  Once we find a value we can use the SetValue method on the PropertyInfo object to assign the property to our business object.

property.SetValue(bizObject, "A property value.", null);

Now that we've automated the process of loading our business  objects with values, lets move on to task 2, calling methods on our object.

Task 2 - Calling Methods

Now that we've instantiated our business object using Activator.CreateInstance, and set the all the properties of our objects using LoadProperties() and SetDynamicPropertyValue(), we need to call the methods on our object and ensure they provide the proper result.  Since all of our business objects inherit from a base BusinessObject class this is extremely easy to automate.  Let's first look back at the interface for our business object base class:

public abstract class BusinessObject {
   public BusinessObject() {}
   abstract public int ID { get; set; }  
   abstract
public void Load(string key);
   abstract public bool Save();
   abstract public bool Delete();
}

Since we already have an instance of a BusinessObject class we can call Save, Load, and Delete.  The base class provides a common interface that allows us to write code in our base test class in a generic fashion.  Rather then instantiating particular types of business objects we simply create an instance of our class using Activator.CreateInstance and then call the necessary methods.

protected
abstract Type BusinessObjectType { get; } 

/// <summary>
/// Test that the business object can be saved.
/// </summary>
[Test]
public virtual void CanBeSaved() {
   Save(BusinessObjectType);
}

/// <summary>
/// Test if the BusinessObject can be deleted.
/// </summary>
[Test]
public
virtual void CanBeDeleted() {
   BusinessObject bizObject = Save(BusinessObjectType);
   Assert.IsTrue(bizObject .Delete(), "The object could not be deleted.");
}


///
<summary>
/// Helper method for saving and business object.
/// </summary>
/// <param name="bizObjectType">The type of business object to save.</param>
protected BusinessObject Save(Type bizObjectType) {
   BusinessObject bizObject = (BusinessObject) Activator.CreateInstance(BusinessObjectType);
   LoadProperties(bizObject);
   SaveAsIs(bizObject);
    return bizObject;
}


///
<summary>
/// Helper method for saving and business object in its current state.
/// </summary>
/// <param name="bizObject">The <c>BusinessObject</c> to save.</param>
private
BusinessObject SaveAsIs(BusinessObject bizObject) {
   Assert.IsTrue(bizObject.Save(), "The " + bizObject.GetType().Name + " could not be saved.");
    return bizObject;
}

Task 3 - Comparing Objects

Now that we've successfully figured out how to load data into our objects as well as call the necessary methods on our objects we're only one step away from having all the pieces in place for the automation of our CRUD unit tests.  The final piece of the puzzle is the comparison of objects.  In order for us to ensure the properties assigned to our objects are persisted to the database properly we need to have the ability to compare two different objects.  In part 2, this was accomplished using the following code.

public void CanBeRead() {
   Customer savedCustomer =
new Customer();
   // ...set properties
   Assert.IsTrue(savedCustomer.Save());
   // read the customer
   Customer readCustomer = new Customer();
   readCustomer.Load(savedCustomer.CustomerID);
   // check properties of the loaded object against the saved object
   Assert.AreEqual(savedCustomer.ContactName, readCustomer.ContactName, "ContactName properties are not equal.");
   Assert.AreEqual(savedCustomer.ContactTitle, readCustomer.ContactTitle, "ContactTitle properties are not equal.");
   Assert.AreEqual(savedCustomer.Address, readCustomer.Address, "Address properties are not equal.");
   // check remaining properties as necessary...
}

As you can see we're manually comparing each property of the savedCustomer object to the same property on the readCustomer object.  We can again use Reflection to retrieve the properties of the business object class to help automate the process of comparing objects.  By looping over each property and comparing the values retrieved from the business objects we can determine if two business objects are "equal."

/// <summary>
/// Test if the business object can be successfully loaded by first saving
/// and then loading and comparing each property value.
/// </summary>
[Test]
public

virtual void CanBeLoaded() {
   BusinessObject savedBizObject = Save(BusinessObjectType);
   BusinessObject bizObject = ((BusinessObject) Activator.CreateInstance(BusinessObjectType);
   bizObject.Load(savedBizObject.ID);

   CompareBusinessObjects(savedBizObject, bizObject);
}

public void CompareBusinessObjects(BusinessObject bizObject, BusinessObject comparedBizObject) {
   // Get all properties and loop through them to ensure the values of the saved
   // business object are the same as the freshly loaded object.
   System.Reflection.PropertyInfo[] properties = BusinessObjectType.GetProperties();
   foreach(PropertyInfo property in properties) {
       if(property.CanWrite && !_ignoredProperties.Contains(bizObjectType.Name + "-" + property.Name))
         Assert.IsTrue(property.GetValue(bizObject,
null).Equals(property.GetValue(comparedBizObject, null)), property.Name + " of the loaded
                                               object is not equal to the value of the saved object.");
     }
   }

}

Wrapping Up

By focusing on the automation of the three common tasks that I was performing in all of my business object unit tests I was able to develop a base unit test that handled the testing of the major CRUD operations on my objects.  By automating this process I am able to focus my attention on the areas that deserve the most attention, the domain layer.  Rather then writing a bunch of very similar tests for testing the CRUD operations on my business objects I'm able to write a bunch of very different tests for the domain layer within my application.  This results in better productivity, and better quality software.

Conclusion

The process of unit testing CRUD operations on business objects can be a very tedious task.  By leveraging a base unit test class we are able to automate this process which greatly reduces the amount of time required to get a suite of unit tests up and running for our objects. 

Although there are a lot of details that I haven't covered in this series of posts on Automating Unit Tests with a Base Class I hope you have seen the advantages that can be gained.  In future posts I'll dive into more of the details and gotcha's that I didn't cover in this first series of articles. 

Did anyone actually make it all the way down  here?   If so please post your thoughts and comments!!

 

# Automating Unit Testing With a Base Class Posts

Saturday, June 19, 2004 1:38 PM by Steve Eichert    
Automating Unit Testing With a Base Class Posts

# re: Part 3: Automating Unit Testing with a Base Class

Saturday, June 19, 2004 5:13 PM by hBifTs    
Good...
It will reduce time :)

thx

# re: Part 3: Automating Unit Testing with a Base Class

Saturday, June 26, 2004 12:21 AM by Chris Garty    
I use OJB.Net (<a target="_new" href="http://ojb-net.sourceforge.net">http://ojb-net.sourceforge.net</a>) to hide persistence from my domain model. I do however have repetative tests to create, load, update, and save my objects to ensure my mappings are correct. I'll have to look at using this approach for those tests.

Nice job...

# re: Part 3: Automating Unit Testing with a Base Class

Saturday, June 26, 2004 2:56 AM by Steve    
Chris, I use a persistence layer as well to hide my domain model. Using a approach similar to the one I discussed here has allowed me to automate a lot of the testing of the basic operations. Helps my focus my attention on other areas that are in more need of the testing efforts!

# re: Part 3: Automating Unit Testing with a Base Class

Friday, March 30, 2007 2:06 AM by Nowzarth    
Hi Every Body
can any body send me the working sample code that is been discussed in the Part 3 Automating Unit Testing with a Base Class .
Email - nowzarthbawa@yahoo.com

# re: Part 3: Automating Unit Testing with a Base Class

Wednesday, April 04, 2007 6:54 PM by Dek Pava    
Agreed with Nowzarth, that would be nice if the working sample available to play with!

# re: Part 3: Automating Unit Testing with a Base Class

Monday, April 16, 2007 7:50 PM by ff    
ff

# re: Part 3: Automating Unit Testing with a Base Class

Tuesday, April 17, 2007 4:23 AM by Nowzarth    
huh
seems to be its a impossible task even by Steve it self. i wrote to his personal email and got reply saying, he is not aware this any more and he cud not remeber it.wht a tragedy for such a good blog and an artical.Any way thanks guys
Nowzarth

Post a Comment

 
 
Prove you're not a spammer: 
8 + 7 =