Unit Test? What's That?
"Hey, I'm having a problem with this timesheet application I'm working on. The piece that stores employee info isn't working correctly" I said to our resident alpha geek as he popped his head into the cubicle. "Ok, lemme see. Where are your unit tests?" he said. "uhm, I haven't written them yet" feeling a mental backlash coming. "Well then it works. At least you have nothing to say it doesn't work right?" he says walking away. Mental note - "Write unit tests before starting development". Writing test allows me to see immediate results of every code change. Although I do spend time on developing these tests, I'd rather write them then have to come back later and debug the code without any indication of where exactly the bug may be. Other developers can also use these tests when integrating with my code or maintaining it later.
In the past I have written tests for my web applications, especially when using custom com objects, by simply printing the results to the page and then comparing returned values with expected ones. Later, I began writing applications in Java and needed a testing framework. Simply writing results to the console was not acceptable. So I began to use JUnit, a unit testing framework created for testing Java applications. I loved it. After making changes I could compile then run the unit test to see a wonderful green progress bar or it would fail and the bar would be red. I would do this constantly as I coded. I compiled and tested after almost every change. Some developers might not be comfortable with this, but coming from a web background, seeing immediate results is something I am used to.
Lately I have been developing simple .Net components using C#, trying to get a good grasp on .Net, but haven't had a good way to test. Then my coworker pointed out NUnit, a .Net version of the popular JUnit testing framework available on the NUnit web site. It's based on JUnit , it was written in C#, the current version (1.7 at time of writing) works with the .Net SDK Beta1 release, and it's Open Source.
Example Class
In the following example I will show how use NUnit to test a business object, showing two C# example classes. The first class is named Profile and its intent is to store the users first name, last name and phone number. It implements a ISaveData interface, which has one method, named save(). The Profile.save() method saves the object's current state into a binary file in the specified directory. The second class is named ExtendedProfile and it's intent is to expand on the Profile class and also store the user's address, city, state, postal, and a description. The ExtendedProfile inherits the save() method from it's parent class but the ExtendedProfile.save() method stores the current state of the object into a XML file instead of binary format.
NUnit Setup
Usually I would write a test right after I write the framework for the classes and their method signatures. In this case the classes are already written, but I still need to create tests for them in case I or someone else has to come to back to them later.
First thing I did was downloaded the NUnit testing framework from the NUnit web site and installed it using the supplied instructions. It was relatively simple. I unzipped the package and placed the executables and the supplied dlls into my applications bin directory. There are two executables: NUnit.exe and NUnitConsole.exe. NUnit.exe supplies a GUI with a progress bar and other nice features. The NunitConsole.exe allows the same tests to be run from the console when a GUI isn't needed. There is also a NunitCore.dll included that houses the frameworks core functions. After installation I then ran the included sample test that produces the following output:
If you look at the code for the sample tests you'll see that setting up a framework for testing your classes is pretty easy. First thing that I did was create a test class and add this class to my Shai namespace (same namespace used by my Profile.dll). I also import the System and NUnit packages with the using statements:
namespace Shai
{
using System;
using NUnit.Framework;
I then create my ProfileTest class that will test my Profile class. It inherits from TestCase using the colon ":" operator. I also declare some variables that will be used during the testing:
public class ProfileTest: TestCase
{
protected String _firstName;
protected String _lastName;
protected String _phoneNumber;
protected String _address1;
protected String _address2;
protected String _city;
protected String _state;
protected String _postal;
protected String _description;
protected String _binaryFilePath;
protected String _xmlFilePath;
// basic constructor:
public ProfileTest (String name) : base(name) {}
Then I have to override the frameworks SetUp() method declaring each variable's value for the test and implement the static ITest property "Suite". This allows the testing framework to hook into the class and create all the variables needed for running the tests.
protected override void SetUp()
{
_firstName = "John";
_lastName = "Doe";
_phoneNumber = "202-555-3434";
_address1 = "1600 Penn";
_address2 = "Suite 100";
_city = "Washington";
_state = "DC";
_postal = "20002";
_description = "Large White House";
_binaryFilePath = @"C:\inetpub\wwwroot\asp.net\articles\nunit\profile.bin";
_xmlFilePath = @"C:\inetpub\wwwroot\asp.net\articles\nunit\profile.xml";
}
public static ITest Suite
{
get { return new TestSuite(typeof (ProfileTest));}
}
Simple Tests
Ok, so now my framework is installed and set up. I tested it using the provided examples and I've created the framework for my ProfileTest class. Now I need to test my Profile class to make sure I can set the values for the properties: firstName, lastName, and phoneNumber. In the code below I have a separate test for each property and it's get and set methods that I created. In each test I created a new instance of my Profile class and call it's "set" methods using my local variables, that I declared above in the SetUp() method, then comparing the output of the "get" methods to these local variables. Using AssertEquals(String, String) I compare the two string values to make sure that what I "set" was what I got back when I called the corresponding "get".
// Test Profile class:
public void TestProfileFirstName()
{
Profile profile = new Profile();
profile.setFirstName(_firstName);
String firstName = profile.getFirstName();
AssertEquals(_firstName, firstName);
}
public void TestProfileLastName()
{
Profile profile = new Profile();
profile.setLastName(_lastName);
String lastName = profile.getLastName();
AssertEquals(_lastName, lastName);
}
public void TestProfilePhoneNumber()
{
Profile profile = new Profile();
profile.setPhoneNumber(_phoneNumber);
String phoneNumber = profile.getPhoneNumber();
AssertEquals(_phoneNumber, phoneNumber);
}
AssertEquals() is just one of the testing methods available within the NUnit framework. There are a ton of others that are useful so take a look at the documentation that comes with the NUnit download for more details on what can and can't be tested. If you need something that's not in there you can extend the framework easily to accommodate it.
Testing a Save
Unlike the simple set and get methods for the Profile class the save method is a tiny bit more complex. The saveProfile method serializes the object and saves the instance to a binary file, the file path set in an input variable, using a BinaryFormatter. The load method simply does the same operation in reverse:
public void TestProfileSave()
{
Profile profile = new Profile();
profile.setFirstName(_firstName);
profile.setLastName(_lastName);
profile.setPhoneNumber(_phoneNumber);
profile.saveProfile(_binaryFilePath);
string firstName = profile.getFirstName();
string lastName = profile.getLastName();
string phoneNumber = profile.getPhoneNumber();
Profile savedProfile = profile.loadProfile(_binaryFilePath);
string savedFirstName = savedProfile.getFirstName();
string savePhoneNumber = savedProfile.getPhoneNumber();
string savedLastName = savedProfile.getLastName();
AssertEquals(phoneNumber, savePhoneNumber);
AssertEquals(firstName, savedFirstName);
AssertEquals(lastName, savedLastName);
}
My ExtendedProfile class inherits it's properties and methods from the Profile base class, extending them to include the address1, address2, city, state, postal, and description fields. I then tested these properties using the same methods outlined above. The saveExtendedProfile() method saves the instance properties into a XML file format using the supplied file path. The loadExtendedProfile() method recreates the saved instance by reading the XML file and assigning each node value to it's appropriate property.
public override void saveProfile(String filePath)
{
XmlTextWriter writer = null;
try
{
writer = new XmlTextWriter (filePath, null);
writer.Formatting = Formatting.Indented;
writer.WriteStartDocument(false);
writer.WriteDocType("Profile", null, null, null);
writer.WriteStartElement("Profile");
writer.WriteElementString("FirstName", _firstName);
writer.WriteElementString("LastName", _lastName);
writer.WriteElementString("PhoneNumber", _phoneNumber);
writer.WriteElementString("Address1", _address1);
writer.WriteElementString("Address2", _address2);
writer.WriteElementString("City", _city);
writer.WriteElementString("State", _state);
writer.WriteElementString("Postal", _postal);
writer.WriteElementString("Description", _description);
writer.WriteEndElement();
writer.Flush();
writer.Close();
}
catch(Exception e)
{
Console.WriteLine ("Exception: {0}", e.ToString());
}
}
public override Profile loadProfile(String filePath)
{
ExtendedProfile profile = new ExtendedProfile();
XmlTextReader reader = new XmlTextReader(filePath);
try
{
while (reader.Read())
{
switch (reader.Name)
{
case "FirstName":
String xmlFirstName = reader.ReadElementString("FirstName");
profile.setFirstName(xmlFirstName);
break;
case "LastName":
String xmlLastName = reader.ReadElementString("LastName");
profile.setLastName(xmlLastName);
break;
case "PhoneNumber":
String xmlPhoneNumber = reader.ReadElementString("PhoneNumber");
profile.setPhoneNumber(xmlPhoneNumber);
break;
case "Address1":
String xmlAddress1 = reader.ReadElementString("Address1");
profile.setAddress(xmlAddress1, "");
break;
case "City":
String xmlCity = reader.ReadElementString("City");
profile.setCity(xmlCity);
break;
case "State":
String xmlState = reader.ReadElementString("State");
profile.setState(xmlState);
break;
case "Postal":
String xmlPostal = reader.ReadElementString("Postal");
profile.setPostal(xmlPostal);
break;
case "Description":
String xmlDescription = reader.ReadElementString("Description");
profile.setDescription(xmlDescription);
break;
}
}
}
catch (Exception e)
{
Console.WriteLine ("Exception : {0}", e.ToString());
}
return profile;
}
So I now have to make sure that all the instance information saved in the xml file can be extracted and the state of the instance recreated at a later time. So I created a TestExtendedProfileSave() methid which test both the save and load methods by creating an ExtendedProfile class instance, setting the property values, saving the state of the instance into a XML file, recreating the state of this object in another instance using the load method, then finally comparing the values of each objects properties:
public void TestExtendedProfileSave()
{
ExtendedProfile extendedProfile = new ExtendedProfile();
extendedProfile.setFirstName(_firstName);
extendedProfile.setLastName(_lastName);
extendedProfile.setPhoneNumber(_phoneNumber);
extendedProfile.setAddress(_address1, _address2);
extendedProfile.setCity(_city);
extendedProfile.setState(_state);
extendedProfile.setPostal(_postal);
extendedProfile.setDescription(_description);
string firstName = extendedProfile.getFirstName();
string lastName = extendedProfile.getLastName();
string phoneNumber = extendedProfile.getPhoneNumber();
string address1 = extendedProfile.getAddress1();
string address2 = extendedProfile.getAddress2();
string city = extendedProfile.getCity();
string state = extendedProfile.getState();
string postal = extendedProfile.getPostal();
string description = extendedProfile.getDescription();
// save the profile:
extendedProfile.saveProfile(_xmlFilePath);
// load the profile we just saved above from xml file:
Profile savedProfile = extendedProfile.loadProfile(_xmlFilePath);
ExtendedProfile savedExtendedProfile = (ExtendedProfile)savedProfile;
string savedFirstName = savedExtendedProfile.getFirstName();
string savedLastName = savedExtendedProfile.getLastName();
string savedPhoneNumber = savedExtendedProfile.getPhoneNumber();
string savedAddress1 = savedExtendedProfile.getAddress1();
string savedCity = savedExtendedProfile.getCity();
string savedState = savedExtendedProfile.getState();
string savedPostal = savedExtendedProfile.getPostal();
string savedDescription = savedExtendedProfile.getDescription();
// compare saved field values with original field values:
AssertEquals(firstName, savedFirstName);
AssertEquals(lastName, savedLastName);
AssertEquals(phoneNumber, savedPhoneNumber);
AssertEquals(address1, savedAddress1);
AssertEquals(city, savedCity);
AssertEquals(state, savedState);
AssertEquals(postal, savedPostal);
AssertEquals(description, savedDescription);
}
During the entire writing/coding of this article I used NUnit as a benchmark for testing my application. Whenever I made a change to my classes I would compile then immediately, go to NUnit and test the ProfileTest.dll to make sure it still worked. At 5:00 am in the morning when your senses dull this can be quite valuable. Sometimes I would code the test before I coded the method, as is the case for the saveExtendedProfile() method. I knew that when I got a solid green bar I would be finished. This tool has become an important part of my toolbox as it should yours too. The importance of testing cannot be stressed enough, and with the increased complexity in .Net, compared to previous versions, it will become even more important for producing solid code.
So go to the NUnit web site for the latest version of NUnit and check it out. At the very least it's worth a look. It will save you endless hours of debugging/QA.
Source Code
I have attached a zip file with this article, my source files, and the installation instructions. You need the .Net Beta1 SDK in order to compile and run the example application.