If you’ve ever written code, you’ve probably written a unit test. If you haven’t written unit tests, you should start.
Now.
Really.
Unit tests let you quickly verify that your code is operating in a predictable fashion. When you make changes down the road, you re-run the same unit tests to make sure nothing broke.
In many cases, you write the unit tests first. Define what your code will do, decide what objects you will use/make to accomplish that, then write a test. At this stage, the test will fail – but you have a place to start. Now write your code until it passes the test and you can be certain your code, however inelegant it might be, is doing what you expect.
Sometimes, though, you don’t write the unit tests first. You go back days, weeks, even years later and try to add unit tests to your code. In most cases, this will be next to impossible because of the way the code is written. A function with direct calls to the database needs to be rewritten. File access needs to be abstracted into an object.
It’s tricky, but makes your code more maintainable in the long run.
Database Update Web Service
I was recently tasked with updating the data persistence routines in a legacy web service that lacked unit tests. My first task was to build out the new test scenarios before changing any code – make sure everything meets expectations before changing those expectations.
The [cci]Save()[/cci] method was the powerhouse of the system. It took in:
- The ID of the client sending data
- A checksum password to verify the client
- A local filesystem path for a log file
- The IP address of the client
The object containing the [cci]Save()[/cci] method also contained a local [cci]data[/cci] object populated from an XML file posted to the server.
Initially, the method looked something like this:
public bool Save(Int32 ClientID, String Password, String sAppData,String IPAddress)
{
try
{
if (Password.CompareTo(GetPasscode(ClientID, Agency.Date)) != 0)
{
throw new Exception("Invalid ClientID!");
}
string Filename = sAppData + "\\" + ClientID.ToString() + ".xml";
//-----Check to see if file is from yesterday or today-----
if (File.Exists(Filename))
{
FileInfo info = new FileInfo(Filename);
//----If File date is today, then create ClientID file
if (info.LastWriteTime.Date.CompareTo(DateTime.Today.Date) == 0)
{
int nCount = 1;
do
{
Filename = sAppData + "\\"
+ ClientID.ToString() + "-"
+ nCount.ToString() + ".xml";
nCount++;
} while (File.Exists(Filename));
}
else //----if FileDate is not today, wipe out all data for that client id
{
File.Delete(Filename);
//---Delete all iterations---
String[] Files = Directory.GetFiles(sAppData + "\\", ClientID.ToString() + "-*.xml");
foreach (String file in Files)
File.Delete(file);
}
}
if (!Save(Filename))
throw new Exception("Unable to Save File!");
File.AppendAllText(sAppData + "\\UploadSysInfo.log", "Pre New Data\r\n");
UploadInfoDataContext data = new UploadInfoDataContext();
File.AppendAllText(sAppData + "\\UploadSysInfo.log", "Post New Data\r\n");
data.SaveToDB(this, IPAddress);
File.AppendAllText(sAppData + "\\UploadSysInfo.log", "Post SaveToDB\r\n");
}
catch(Exception e)
{
File.AppendAllText(sAppData + "\UploadSysInfo.log", "Error: " + e.Message + "\r\n");
throw;
}
return true;
}
Unfortunately, you can’t just invoke this method and test that it works. The method talks to the file system directly, then calls a data-heave [cci]SaveToDB()[/cci] method that talks directly to several SQL stored procedures. Invoking this version of the method directly from a test routine will corrupt your data and cause a while host of other unintended effects.
Instead, we can abstract what dependencies the system does have – primarily the file system and database. By wrapping that access in secondary objects, we can mock those objects and inject our mocks into the system.
Mocking is a fancy way of saying we override the default behavior and prevent the code from actually touching the file system or database by short-circuiting it in a specifi way.
First, we change the signature of the [cci]Save()[/cci] method so that it also takes in an object of type [cci]IStatusLog[/cci]. This is an object implementing a specific interface for writing to a static text log:
public interface IStatusLog
{
void Write(string Message);
}
Our test code passes in a fake object that implements [cci]IStatusLog[/cci] but doesn’t actually do anything. Any calls to member methods on the mocked object will go into the abyss. The program won’t crash, and our test will actually test what we want to pass.
We also add an [cci]IXmlDataWriter[/cci] object to wrap routines that write our posted XML data to disk and an [cci]IUploadInfoDataContext[/cci] interface that wraps our database access methods. In normal use (i.e. production), the system creates real objects for these interfaces and passed them to our worker method:
public bool Save(Int32 ClientID, String Password, String sAppData, String IPAddress, IStatusLog statusLog)
{
this.Log = statusLog;
this.Log.Write("Pre New Data");
IUploadInfoDataContext data = new UploadInfoDataContext();
this.Log.Write("Post New Data");
return this.Save(
ClientID, // ClientID
Password, // Password
sAppData, // sAppData
IPAddress, // IPAddress
statusLog, // statusLog
new XmlDataWriter(sAppData, ClientID), // writer
data // data
);
}
But the real work is done in another method, an overload of [cci]Save()[/cci] that takes in our objects that implement [cci]IStatusLog[/cci], [cci]IXmlDataWriter[/cci], and [cci]IUploadInfoDataContext[/cci]. In production, these are real objects that touch the file system and database. In testing, they’re mocks that return whatever values we need them to return.
But since both cases use the same interfaces, they expose the same methods and properties. We can write our [cci]Save()[/cci] method tied to these dependencies and rely on it both in production and in testing.
public bool Save(Int32 ClientID, String Password, String sAppData,
String IPAddress, IStatusLog statusLog, IXmlDataWriter writer,
IUploadInfoDataContext data)
{
this.Log = statusLog;
try
{
if (Password.CompareTo(GetPasscode(ClientID, Agency.Date)) != 0)
{
throw new ApplicationException("Invalid ClientID!");
}
// Save object to XML file
if (!writer.WriteData(this)) throw new ApplicationException("Unable to Save File!");
data.SaveToDB(this, IPAddress, this.Log);
this.Log.Write("Post SaveToDB");
}
catch (ApplicationException e)
{
this.Log.Write("Error: " + e.Message);
throw;
}
return true;
}
This method is the one we’re testing. It’s the one that contains the actual logic to validate client data, save our passed object to disk, and update the database. Everything else has been merely abstracting the underlying framework in such a way that we can inject phoney dependencies in a test environment.
This test method, for example, passes in a mocked log writer, a mocked data writer, and a mocked database context. However it still validates that the client ID makes it through our password check, that the mock database records data, and that the [cci]Send()[/cci] method returns true.
[TestMethod]
public void Save_Returns_True_After_Writing_To_Database()
{
Mock
Mock
Mock
UploadInfo info = new UploadInfo();
info.Agency = new AgencyInfo()
{
Date = "12/1/2012"
};
dataWriter.Setup(x => x.WriteData(info)).Returns(true);
dataContext.Setup(x => x.SaveToDB(info, It.IsAny
bool status = info.Save(
28, // ClientID
"7DC1cC1", // Password
"AppData", // sAppData
"127.0.0.1", // IPAddress
statusLog.Object, // statusLog
dataWriter.Object, // writer
dataContext.Object // data
);
Assert.IsTrue(status);
}
Abstracting dependencies into interfaces is a valuable tool in any application developer’s toolbox. It just so happens that the practice is somewhat more elegant in C# than other languages. But understanding how, when, and why to use interfaces is a lesson every developer should learn.