Part 2: Automating Unit Tests with a Base Class

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]

# Part 2 Follow Up: But those tests suck!?!

Sunday, June 06, 2004 3:37 AM by Steve Eichert    
Part 2 Follow Up: But those tests suck!?!

# Part 3: Automating Unit Testing with a Base Class

Saturday, June 19, 2004 1:12 PM by Steve Eichert    
Part 3: Automating Unit Testing with a Base Class

# 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 2: Automating Unit Tests with a Base Class

Tuesday, June 22, 2004 6:57 AM by Thomas Eyde    
Why don't you let the Load() method return false to indicate it doesn't exist? Everything else could be an exception.

I use the ID trick myself, but it's not very OO.

The following has nothing to do with your article, but you are unit testing, right? Why do you wrap your fields with getters and setters when you don't need them?

# re: Part 2: Automating Unit Tests with a Base Class

Tuesday, June 22, 2004 7:23 AM by Steve    
Thomas, I could definitely make the Load method return false for indicating a record doesn't exists. The "architecture" I'm using in this series of architecture isn't what I'm actually using in my apps. I have a framework which handles a lot of that so the architecture discussed could certainly use some enhancements/refinements.

Regarding the setters/getters, I usually have getters and setters for my properties for versioning reasons. I usually end up needing them so I have my code generator product code with them. Again, maybe not always needed.

# Automating Unit Testing With a Base Class

Tuesday, July 06, 2004 4:06 PM by bigsea    
Automating Unit Testing With a Base Class

# re: Is It Time To Give up on the blogs.msdn.com Feed?

Saturday, December 31, 2005 2:48 PM by Thom Lawrence    
I always quite liked having it there just in case something turned up in a search or on the off chance that someone stood out. But the signal to noise ratio is getting lower and lower.

Conversely, I've found that the Planet ThoughtWorks feed gets more interesting the more ThoughtWorkers are aggregated there.

I dunno if there's a critical mass that turns a corporate aggregate feed into mush. It'd be an interesting experiment - are there busy Sun/IBM/Apple/Google aggregate feeds?

# re: Part 2: Automating Unit Tests with a Base Class

Thursday, September 13, 2007 12:29 PM by Jorge De Castro    
I notice your unit-tests are making lots of redundant assertions. It is generally better to have one assert per method being tested (otherwise you have tests full of noise). In your case, testUpdate doesn't need to assert that Create succeeds -you have a separate test for that.
Testing the Update method should be only that: asserting *Update* behaves as spec'ed.

Cheers
j

Post a Comment

 
 
Prove you're not a spammer: 
0 + 1 =