I have just embarked on a journey of test-first development. This means you write a test for your block of functionality either before you code the class or while you code it. Ok, it means "before", but writing it at the same time also works well. Writing the test after is bad. why? First, you may have written something other than your requirements in your code and now your test reflects your code rather than your requirements. Second, you'll never do it! Once the code is done, you are unlikely to go back and write a test for it.
So I wrote some simple tests to get me started with NUnit. I have an entity class that gathers data from oracle and creates an instance from serialized xml. How do I test this? First test is to create an instance and make sure it is not null. Then, I can check some known values. I have a second class which uses this classes data to create a webrequest to Google and retrieve some ads. Finally, I pass my first class instance to my second class and test that it has the expected return.
I decided to include the creation and gathering of the ad in a seperate internal class that could be shared by several test fixtures (or test groups). This way, I can maintain an atomic, independent test paradigm yet not cut and paste the same code from test to test. I was very pleased at the result. Take a look at the code and read on.
using System;
using System.Reflection;
using NUnit.Framework;
using MyNamespace.Ads.Google;
using System.Xml;
using System.IO;
namespace MyNamespace.Ads.Google.NUnitTest
{
///
/// Test the ceation of the AdSenseAd entity class
///
///
[TestFixture]
public class TestAdSense
{
private AdSenseAd _ada;
[TestFixtureSetUp]
public void SetUpAdSense()
{
_ada = AdSetUp.GetAdSenseAd();
}
[Test]
public void AdSenseAdNotNull()
{
Assert.IsNotNull(_ada);
}
/*[Test]
public void TestAdSenseAdTreatmentA()
{
Assert.AreEqual(_ada.DisplayStyle.ToString(),"TreatmentA");
}
*/
}
///
/// Test the ceation of the AdSenseResponse class
///
///
[TestFixture]
public class TestAdSenseResponse
{
private AdSenseAd _ada;
private AdSenseResponse _asr;
[TestFixtureSetUp]
public void SetUpAdSense()
{
_ada = AdSetUp.GetAdSenseAd();
_asr = AdSenseResponse.CreateObject(_ada,"127.0.0.1","Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.1.4322)");
}
[Test]
public void AdSenseResponseNotNull()
{
Assert.IsNotNull(_asr);
}
[Test]
public void AdSenseCountLessThanRequesteNumber()
{
Assert.IsTrue(_asr.AdSenseAdData.NumberOfAds>_asr.AdsCollection.Count);
}
[Test]
public void AdSenseCountsMatch()
{
Assert.AreEqual(_asr.AdSenseAdData.NumberOfAds,_asr.AdsCollection.Count);
}
}
[TestFixture]
public class testWarehouseLog
{
[Test]
public void WarehouseWriteTest()
{
XmlNode xmlRoot = AdSetUp.GetTestDataDocument();
//set the conn string
string sTargetQueue = xmlRoot.SelectSingleNode("//MSMQPath").InnerText;
WarehouseEventLog.SetMSMQPath(sTargetQueue);
WarehouseEventLog whel = new WarehouseEventLog();
string sMe = WarehouseEventLog.GenerateWarehouseMessage("2","999","999","this_is_an_automated_test=true");
WarehouseEventLog.LogWarehouseMessage(sMe);
}
[Test, Ignore("reading the written message is not implemented yet")]
public void WarehouseReadTest()
{
WarehouseEventLog whel = new WarehouseEventLog();
string sMe = WarehouseEventLog.GenerateWarehouseMessage("2","999","999","this_is_an_automated_test=true");
WarehouseEventLog.LogWarehouseMessage(sMe);
}
}
internal class AdSetUp
{
public static AdSenseAd GetAdSenseAd()
{
XmlNode xmlRoot = GetTestDataDocument();
//set the conn string
string sConnectString = xmlRoot.SelectSingleNode("//AdvertiserConnString").InnerText;
AdDataAccess.ConnectionString = sConnectString;
//get the AdId to test against
int iAdId = int.Parse(xmlRoot.SelectSingleNode("//ValidAdSenseAdId").InnerText);;
//get our ad sense ad
AdSenseAd ada = new AdSenseAd(iAdId);
return ada;
}
public static XmlNode GetTestDataDocument()
{
XmlDocument xml = new XmlDocument();
string sBinPath = GetApplicationDirectory();
int iIdxOfBin = sBinPath.IndexOf(@"\bin");
string sXmlPath = sBinPath.Substring(0,iIdxOfBin) + @"\xml\AdSenseInfo.xml";
xml.Load(sXmlPath);
return xml.DocumentElement;
}
public static string GetApplicationDirectory()
{
return Path.GetDirectoryName(Assembly.GetExecutingAssembly().CodeBase).Replace(@"file:\", "");
}
}
}
There were a few things that unexpectedly came out of this exercise.
I discovered where I had inappropriate dependencies in my code. For instance, I required web.config to establish a connection string. This is common practice. It is very easy to import the System.Configuration namespace and utilize the provided library. However, it is just as easy to provide a property accessor to your connection string and abstract the data access into a seperate class. There you can check for a null connection string and try to get it from appSettings if its missing. My entity class knows how to map method parameters to command parameters and execute the appropriate stored proc. A purist might prefer to use a factory class and make the entity class "dumb". Both approaches work so I contend that they are both right. However, it is plain wrong to have a connection string hard-coded in your entity class and nearly as bad to get it from web.config from that entity class. A data access class is a much more appropriate place to dictate where the connection string comes from and how to connect. I wrestled with the idea of moving all the data access out of the entity class but that would complicate my code and offer little benefit for my purpose...
I also discovered that I couldn't do a true test of our message queue lifecycle because my local app was writing to the local MSMQ and I had no receiver running locally. The receiver takes the messages from the queue and places them in a SQL Server. I needed to write to a queue where a receiver was installed and running. I created a static method to set the server and queue path in message sending class. Now I can override the default target and write to a specified server where the message should be picked up and available in the SQL Server table.
My messages never made it to the database. My test failed which means my testing succeeded. I had no idea that my messages were not being delivered. They showed up as expected in my local public queue and were successfully sent to the remote queue and picked up by the listener. However, the message were not delivered to the database. It is critically important that we can report on ad views and click throughs. Without this data, the product would have been a disaster. I can now begin my analysis to discover the problem and resolve it. The beautiful thing about automated testing with NUnit is that I will know I have been successful when the red light turns to green (when the MSMQ read test succeeds). Running a test takes a few seconds. Any change I make in the future can be delivered with confidence the moment I have a full row of green. In the past, I would have to open Query Analyzer and re-type the same old, tired query to see if my records had arrived. This will save me minutes per change and many, many hours over the course of several projects. For me, this is time I have recoverd but for my clients, it is money back in their pockets or a reduced cost on their software products.
Flaws in design are easy to identify when you have time to sit and think. When you are busy working toward a deadline, you have to be even more resolute in your conventions. Most people get sloppy. Worse, they get sloppy on your code! Creating automated tests is mildly time consuming up front. However, the payoff comes in more reliable code, better design, time saved in future modifications and self-review. You will find changes easier to make because the program had to support a testing client and that testing client will reduce the chain of defects that aren't always apparent to the developer making the change.
Overall, I think automated testing is the holy grail of quality software delivery. It doesn't matter how fabulous an architecture you design, some developer will come along and hack it. If he doesn't break anything, great, but on that tenth hack, when something down the line is causing trouble, you will have to go in to identify it. If you have a test suite that can reduce that search to a single function, imagine how much less stress you will feel having to repair the damage? Just one of a million incentives to write your own tests.