Automated Unit Testing
Scott Hanselman has been experimenting with Ruby, Watir, and NUnit. His goal is what all of us web guys are after, a better way to perform unit testing of the web interface. We have NUnit to test all of our business objects, data acess code, and controller logic but we don’t have any real good options for performing unit testing on the web presentation layer. Scott developed a WaitrAssert class for integrating Waitr into NUnit which he uses to do all the testing within his application. Definitely cool stuff…
I've recently written a series of posts on the process of automating the unit testing of CRUD operations on business objects.
In future posts I'll dive into some of the details which I didn't go into such as how to set the allowable values for a property, how to ignore a property when comparing objects, how to set a property value as unique, as well as how to manage relationships among objects. Look for a zip file containing a running example in the next couple of days.
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!!
Part 1 of Automating Unit Tests with a Base Class provided a brief summary of the problems we run into while writing unit tests for our business objects, and discussed why it's important that the database be included. In this post I'm going to describe the process of writing unit tests for business objects. In part 3 I will detail how we can automate the testing of our CRUD operations via a base unit test class.
Testing Business Objects
CRUD operations are the primary operation we carry out on our business objects. Each of these CRUD operations involves a set of steps that is performed for each type of object. To ensure that the operations work for all of our business objects it is important to have unit tests that verify the expected results are provided when we perform each operation. Before diving into the specifics of how we test our business objects lets first take a look at the interface of our business objects (for the purposes of this article).
1 public abstract class BusinessObject {
2
3 public BusinessObject() {
4 }
5
6 abstract public void Load(string key);
7 abstract public bool Save();
8 abstract public bool Delete();
9 }The BusinessObject class is used as the base class for our business objects. The class provides the common interface for our business objects. We can see that our business objects support saving themselves to the data store (Create, Update) via the Save() method, deleting themselves from the data store (Delete) via the Delete() method, and finally loading themselves from the data store (Read) via the Load() method.
Now that we have overviewed the base BusinessObject class let's take a look at an example Customer object.
1public class Customer : BusinessObject {
2
3 public Customer() : base() {}
4
5 public override bool Delete() {
6 // Create DAL, and remove from the database...
7 return true;
8 }
9
10 public override bool Save() {
11 // Create DAL, and save to the database...
12 return true;
13 }
14
15 public override void Load(string customerID) {
16 // Create DAL, and load from the database...
17 }
18
19 public string CustomerID {
20 get { return _customerID; }
21 set { _customerID = value; }
22 }
23
24 public string CompanyName {
25 get { return _companyName; }
26 set { _companyName = value; }
27 }
28
29 public string ContactName {
30 get { return _contactName; }
31 set { _contactName = value; }
32 }
33
34 public string ContactTitle {
35 get { return _contactTitle; }
36 set { _contactTitle = value; }
37 }
38
39 public string Address {
40 get { return _address; }
41 set { _address = value; }
42 }
43
44 public string City {
45 get { return _city; }
46 set { _city = value; }
47 }
48
49 public string Region {
50 get { return _region; }
51 set { _region = value; }
52 }
53
54 public string PostalCode {
55 get { return _postalCode; }
56 set { _postalCode = value; }
57 }
58
59 public string Country {
60 get { return _country; }
61 set { _country = value; }
62 }
63
64 public string Phone {
65 get { return _phone; }
66 set { _phone = value; }
67 }
68
69 public string Fax {
70 get { return _fax; }
71 set { _fax = value; }
72 }
73
74
75 private string _customerID = String.Empty;
76 private string _companyName = String.Empty;
77 private string _contactName = String.Empty;
78 private string _contactTitle = String.Empty;
79 private string _address = String.Empty;
80 private string _city = String.Empty;
81 private string _region = String.Empty;
82 private string _postalCode = String.Empty;
83 private string _country = String.Empty;
84 private string _phone = String.Empty;
85 private string _fax = String.Empty;
86 }
The Customer class provides a set of properties for managing important information about the customer, as well as an implementation for saving, deleting, and loading customers from the data store. The actual implementation of the save, delete, and load is not the focus of this article, however, the fact that each business object implements the interface for performing these operations via the BusinessObject class is important. By inheriting from the BusinessObject class our objects are agreeing to implement the interface we've defined. When we begin looking at how we can automate the unit tests for the CRUD operations on our objects this common interface will prove itself extremely valuable.
Unit Testing CRUD Operations
Now that we've covered the basics of our business objects lets take a quick look at the details surrounding the unit testing of the CRUD operations for our objects. Below is a list of the four CRUD operations and the process we follow for unit testing each operation. I've included a sample test for each operation that provides one (of many) ways of implementing the tests.
Create -
1. Instantiate an instance of the object.
2. Set the properties of the object with valid values.
3. Call the Save() method on the object, checking that the save succeeds.
1 public void CanBeSaved() {
2
3 Customer customer = new Customer();
4 customer.ContactName = "Steve Eichert";
5 customer.Address = "221 South North West Ave.";
6 customer.City = "Philadelphia";
7 customer.Region = "PA";
8 // ...set other properties
9
10 Assert.IsTrue(customer.Save());
11
12 }
Read -
1. Do Create.
2. 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.
1 public void CanBeRead() {
2
3 Customer savedCustomer = new Customer();
4 // ...set properties
5
6 Assert.IsTrue(savedCustomer.Save());
7
8 // read the customer
9 Customer readCustomer = new Customer();
10 readCustomer.Load(savedCustomer.CustomerID);
11
12 // check properties of the loaded object against the saved object
13 Assert.AreEqual(savedCustomer.ContactName, readCustomer.ContactName, "ContactName properties are not equal.");
14 Assert.AreEqual(savedCustomer.ContactTitle, readCustomer.ContactTitle, "ContactTitle properties are not equal.");
15 Assert.AreEqual(savedCustomer.Address, readCustomer.Address, "Address properties are not equal.");
16
17 // check remaining properties as necessary...
18 }
Update
1. Do Read
2. Change the properties of the object.
3. Call the Save() method on the object.
4. 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.
1public void CanBeUpdated() {
2
3 // read the customer
4 Customer updateCustomer = new Customer();
5 updateCustomer.Load("CUST-99");
6 updateCustomer.ContactName = "New Contact Name";
7 // ...change other properties as necessary
8
9 Assert.IsTrue(updateCustomer.Save());
10
11 // read the customer
12 Customer readCustomer = new Customer();
13 readCustomer.Load(updateCustomer.CustomerID);
14
15 // check properties of the loaded object against the saved object
16 Assert.AreEqual(updateCustomer.ContactName, readCustomer.ContactName, "ContactName properties are not equal.");
17
18 // check remaining properties as necessary...
19
20}
Delete -
1. Do Create
2. Call Delete() on the object.
3. Try reading the object back out of the data store and ensure we get a null object (since its deleted).
1public void CanBeDeleted() {
2
3 Customer customer = new Customer();
4 // ... set properties
5
6 Assert.IsTrue(customer.Save());
7
8 Assert.IsTrue(customer.Delete());
9
10 // read the customer
11 Customer readCustomer = new Customer();
12 readCustomer.Load(customer.CustomerID);
13
14 // ... check that the readCustomer is not valid through some mechanism
15 Assert.AreEqual("-1", customer.CustomerID, "The deleted object still exists!");
16
17}
To ensure each business objects is successfully handling these four CRUD operations we need to write four (at a minimum) unit tests for each business object in our system. On a decent sized project this forces us to write a lot of redundant code.
Rather then writing tests after test for every business object, we can automate the process of testing the basic CRUD operations by creating a base unit test class. With a base test class in place we can focus on testing the pieces of the application that need it the most rather then the rudimentary CRUD operations. By incorporating a base test class into our testing framework we can remove the burden of writing tests for each CRUD operations and instead focus on writing tests for the business logic within our application.
Next up: How do we accomplish our goal of automating our tests?
[Now Playing: The Wallflowers - The Wallflowers - After the Black Bird Sings (04:4
]
Summary:
Unit testing is quickly gaining traction within the .NET community. By unit testing business objects developers are finding they are able to reduce the number of bugs they write, and improve the overall design of their applications. The benefits of unit testing are well documented; however, unit testing objects doesn’t come free of charge. Writing unit tests for each method of a business object is a time consuming task and often requires a lot of repetitive code. This article discusses a method that can help reduce the leg work required to create a suite of unit tests for testing basic CRUD (create, read, update, delete) operations on business objects. Removing the overhead of writing individual tests for each of the CRUD operations allows the unit testing effort to be focused on the area that it's most needed, the business logic of the application.
Note: This article focuses on automating the unit testing of objects in a domain model and ignores how some of the techniques could be used to test an application using a manager/service model. Many of the ideas can also be applied to applications using the service model which I'll look to address in future postings/articles.
What is unit testing?
Before getting too far, let's briefly overview what unit testing encompasses. Unit testing is the process of creating a set of automated tests that validate that the individual components (units) of an application behave as expected. Each unit is tested via a test class, a “unit test.” The unit test performs a series of assert’s to ensure each method exposed publicly provides the correct behavior when it’s provided different sets of information.
Test driven development focuses on using unit tests to drive the development of software. Rather then testing objects after they have been written, the tests for each class are written before the actual implementation. Test driven development forces developers to design their applications with the users of the API in mind. It results in a de-coupled API that is clearly focused on the tasks that the end developer will need to accomplish.
Creating automated tests for the objects within an application helps reduce bugs, and can result in a cleaner overall design for the application. Both good things.
The Problem Area
Although unit testing provides many benefits it doesn’t come free of charge. Unit testing objects takes time. Over the course of a project unit testing will often end up saving time, however, the up front costs can’t be ignored. Writing unit tests for every method of every business objects is time consuming and can involve a lot of repetitive coding.
Each business object has a common set of operations. These operations are typically referred to as CRUD (Create, Read, Update, Delete). Each object needs to be able to be saved to the data store, read out of the data store, updated, and removed from the data store. Every business object implements these features, and should have a unit tests validating that each of these operations performs as expected. Although the implementation of each operation is similar across all business objects, it is different enough to require a separate unit test method for every single business object. In a large application this adds hundreds of methods for testing a set of functionality which is consistent across all of our objects. This forces us to write a lot of repetitive code. But should it?
Why test with the database?
Before moving on to address whether testing the basic CRUD operations on business objects should require a lot of repetitive coding let me first address the age old question: “Should a database be included in my unit tests?”
Many developers within the Unit Testing / Test Driven Development (TDD) community believe that unit tests should not include the database. Including the database in unit testing and TDD efforts does slow down the process and can interrupt our coding “flow“, however it has benefits which can be extremely valuable:
- Validate the stored procedures function as expected.
Provides an overall integration test for the application and database.
Forces you to think about deployment, integration, and setup of the database from the start.
Forces you into a one click build, from scratch, including the database.
Including the database has disadvantages as well:
- Have to spend more time thinking about how to fit the database in with the unit tests.
- Need to write scripts OR DTS packages to setup the database in a clean state.
- Need to worry about cleanup of any data inserted during the running of the unit tests.
- The tests will take longer to run when it’s running against the real database rather then just a Mock object.
Over the last couple of years I’ve stood on both sides. Early on I didn’t know any better so included the database in every aspect of my testing. As I became more familiar with Unit Testing and TDD I slowly began introducing Mock Objects, and stub methods into my unit tests. By reducing the interaction with the database I was able to increase the quality of my designs and get into more of a groove as I wrote my test classes. Unfortunately, I also started noticing more problems with my database and stored procedures. Usually the problem wasn’t significant, a missing parameter on a stored parameter or an improperly set default value for a column. Clearly the database needed to be included in my testing process, but how?
Next up: How do we automate our tests, and how does this compare to how we currently test our business objects
A couple weeks ago I posted a
brief outline of an article which I've been wanting to spend some time writing. Since I haven't had much success setting aside time to actually write the article I'm going to start posting my thoughts and excerpts to this blog in hopes that I actually get most of my thoughts, ideas, and code samples in a central place. I'd appreciate any comments, questions, or insights which you might want to share!