Refactoring to Tests


When presenting on TDD/Unit testing to user groups or code camps, a common question I hear is, “How can I get tests into my existing codebase?”. This is often where people want to put effort when first starting with unit testing and it is a valid one, albeit a bit difficult at times.

The main reason that getting tests into legacy code is difficult is because the manner in which the code was originally written, which is code without tests. The reason that you can’t wave the testing magic wand over your code is because tightly coupled code begets hard to test code. This is not to say that ALL legacy code is hard to test, but in my experience this is often the case. There are a few techniques for getting unit tests into legacy code depending on the circumstances that I will go over in this post.

Unit/Integration/Functional testing

First and foremost it is important to distinguish between unit/integration and functional testing. A common mistake I see newcomers make is to only write one type of tests, usually integration tests which is fine and good, but you’re not really getting the most bang for your buck from testing with only integration tests or only unit tests.

Unit testing is defined on wikipedia as: “A unit is the smallest testable part of an application”. The idea here is to isolate an inidividual piece of funtionality and remove all dependencies on other classes so that you can test it in isolation. To accomplish proper isolation you can use Mocks/Fakes to “mock” out the surrounding dependencies that will narrow the scope of your test to one piece of functionality. Now that we have covered this topic, let’s look some examples.

Tight coupling hell

Here is an example of a class you may find in a legacy codebase that you are trying to get under test:

   1: public class Employee
   2: {
   3:     private IDictionary<int, decimal> _checkAmounts;
   4:  
   5:     public string Fullname { get; set; }
   6:  
   7:     public Employee()
   8:     {
   9:         _checkAmounts = new Dictionary<int, decimal>();
  10:     }
  11:  
  12:     public void Pay(decimal checkAmount)
  13:     {
  14:         int nextCheckNumber = DatabaseProvider.GetNextCheckNumber();
  15:         _checkAmounts.Add(nextCheckNumber, checkAmount);
  16:  
  17:         // Log the payment to an auditing log
  18:         LogAudit(nextCheckNumber, checkAmount);
  19:     }
  20:  
  21:     private void LogAudit(int nextCheckNumber, decimal checkAmount)
  22:     {
  23:         using (StreamWriter streamWriter = File.AppendText("paymentaudit.log"))
  24:         {
  25:             streamWriter.WriteLine(string.Format("Check Number:{0} ${1}, {2}", nextCheckNumber, checkAmount, Fullname));
  26:         }
  27:     }
  28: }
</p>

 

This is a pretty contrived example but it will serve our purpose well. This class is violating the Single Responsibility Principle by doing a couple of different things. Notice that the Employee class is not only in charge of paying an employee but also handles writing out auditing entries to a log file on the file system. For this reason (among others that we will get to shortly), this class is unable to be unit tested properly. This leads to my next point about some unit testing rules.

When doing Unit Testing, there are some simple rules you should follow:

  1. Do not touch files on the file system
  2. Do not touch a database that requires a connection
  3. Do not touch configuration files on the filesystem

These rules are only off the top of my head and are up for debate. Tests that fall into the 3 rules above should be treated as integration tests because they are touching areas that will drastically slow down your tests. Slow/Long running tests should be run by a continuous integration server or on your local dev machine infrequently. Unit tests are meant to be blazing fast. The slower they run, the less likely developers are going to run them on an ongoing basis.

The key to getting legacy code like the above sample under test is to create seams in your code. Once

Code Smell: Fat Controller