A Strategic Case for Unit Testing

There is a certain level of velocity and quality that you will never reach without effective unit testing.

I’ve had an unusual number of queries about unit testing during the past few days. As a result I thought I would put a more complete description together.

Definition

I’m not going to spend a great deal of time to define a unit test. I’ve heard some say, “The smallest piece of code that does meaningful work.” But since I frequently use Test First or TDD (Test Driven Design as one of my friends states) I frequently write a test that doesn’t yet know about the size of the work to be done.

So for my discussion here, it means the use of a “unit test framework” such as MS Test or xUnit to write tests that verify your code works properly.

Exercise Your Code to Keep It Lean

Some Characteristics of a Quality Code Base

Years ago on a Channel 9 ArcCast I discussed quality code and the notion of Quality, Fast, and Simple. As part of that discussion with Ron Jacobs I discussed the following notion – that code complexity is a function of (among other things) Lines of Code (LOC). I described (since this was an audio-only cast) the following diagram:

image

When I say, “Complex” I mean difficult to understand and thus, maintain. We’ve all seen code that was so terse that it was difficult to understand. But the more usual case is that a code base grows and grows. One code base I worked on prior to that ArcCast had over 18,000 lines of code. That’s not really a lot, given what it did, but after the release of .NET 2.0 I was able to shrink that code base to 3,800 lines of code!

I firmly believe that when it comes to code bases, “More is NOT better”. When you maintain it, you have more code to read. New developers have more code to read. More code usually has more bugs. More code usually runs slower.

When did you last delete code?

In large code bases with many developers deleting code seems a rare occasion. How do you know the scope of effect? If you have unit tests, you can actually delete code with confidence – even celebration! Unit tests are a safety net that allow you to understand what code is affected by removing the other code.

image

My observations have been that if you do not have an effective Unit Testing Strategy, you will continually grow. Many code bases are crushed by their own weight. A strong unit testing strategy will allow you to continually exercise your code and keep it lean.

Understanding Unit Tests

What better way to understand unit testing than to write unit tests about unit testing? Many years ago that’s what I did. Here’s what my project looks like:

image

So lets start with a look at a class diagram.

image

Since you really can’t understand unit tests without also understanding its relationship with code and the nature of that code I’ve included code models in addition to test models.

I’ll start with code…

Code

Let me start with the base class – Code. It’s really quite simple. You’ll notice the test was listed first.

Code
  1. public abstract class Code
  2. {
  3.     public abstract UnitTest UnitTest { get; set; }
  4.     public abstract string CodeText { get; set; }
  5. }

Legacy Code

I’ve found Michael C. Feathers book, “Working Effectively With Legacy Code” very useful, particularly his definition of Legacy Code. From memory it was “Any code without automated tests.”

Let’s take a look at the LegacyCode class:

Legacy Code
  1. public class LegacyCode : Code
  2. {
  3.     /// <summary>
  4.     /// Tests? We don’t need no stinking tests…
  5.     /// </summary>
  6.     public override UnitTest UnitTest
  7.     {
  8.         get { return null; }
  9.         set
  10.         {
  11.             var message = “Get lost – by definition “
  12.                 + “(see Michael Feathers “
  13.                 + “‘Working Effectively with Legacy Code’)”
  14.                 + “I do NOT have unit tests.”;
  15.             throw new NotSupportedException(message);
  16.         }
  17.     }
  18.  
  19.     /// <summary>
  20.     /// No logic required… We’re just going to write some code.
  21.     /// </summary>
  22.     public override string CodeText { get; set; }
  23.  
  24. }

You can see that my class throws a NotSupportedException because Legacy Code does not have unit tests.

DO NOT write legacy code.

Moving on to UI (User Interface)…

User Interface Code

User interface code derives from Legacy Code because it seldom has automated tests. Since this exercise assumed that unit tests were your automated tests, it would be legacy code. If you used Coded UI Tests then you could have non-legacy UI Code.

User interface code is hard to unit test. The key here is to limit the amount of code in your presentation tier. When I say presentation tier, I mean the final layer. When using Silverlight or WPF I use the MVVM (Model-View-View Model) pattern and consider the View to be the presentation tier, but both the Model and the View Model can be easily unit tested.

DO minimize the code in your presentation tier.

Here is the UICode class:

UICode
  1. public class UICode : LegacyCode
  2. {
  3.     public override UnitTest UnitTest
  4.     {
  5.         get { return null; }
  6.         set
  7.         {
  8.             var message = “You still need to test, “
  9.                 + “but another strategy is probably best. “
  10.                 + “Minimize this code and perhaps use Coded UI Tests.”;
  11.             throw new NotSupportedException(message);
  12.         }
  13.     }
  14. }

Agile Code

The next class is the Agile Code – simply code that does have unit test.

There are several characteristics you’ll see for the AgileCode such as it is impossible for the UnitTest to be null. This would be the approach you are using if you write your code first, then your test.

DO write unit tests.

Agile Code
  1. /// <summary>
  2. /// Represents Agile – aka Non-Legacy code.
  3. /// </summary>
  4. public class AgileCode : Code
  5. {
  6.     private UnitTest _unitTest;
  7.     public override UnitTest UnitTest
  8.     {
  9.         // Uses the null coalescing operator since this cannot be null
  10.         // for AgileCode.
  11.         get { return _unitTest ?? (_unitTest = new UnitTest { IsUICode = false }); }
  12.         set
  13.         {
  14.             if (ReferenceEquals(value, null))
  15.                 throw new InvalidOperationException(“Agile code, i.e., non-legacy code, really needs unit test unless it is UI code.”);
  16.             _unitTest = value;
  17.         }
  18.     }
  19.  
  20.     private string _codeText;
  21.     public override string CodeText
  22.     {
  23.         get { return _codeText; }
  24.         set
  25.         {
  26.             _codeText = value;
  27.             if (!UnitTest.IsCompleted)
  28.                 RunUnitTestWizard();
  29.         }
  30.     }
  31.  
  32.     private void RunUnitTestWizard()
  33.     {
  34.         // If you’re here, then you wrote your code first…
  35.         // Now write your unit test.
  36.     }
  37. }

Why the name AgileTest?

I could have called this code, TestedCode or UnitTestedCode, but I wanted to make a point. Here are some questions to consider:

  • Can you have Agile software development if it takes you longer to fully test your code than the length of your iteration?
  • Can you ever have an Agile development project without an effective unit testing strategy?

I contend that without a unit testing strategy you cannot truly have an Agile Software Development Process.

DO use unit testing if you plan to use Agile practices.

Test First Code

The final Code class is the TestFirstCode. As the name implies the test is written before the code. This is the “Socratic Method” for writing code. Don’t just jump to the answer, ask questions that lead you to the answer. I’m continually amazed at the clarity in requirements that I gain from this approach. If I have ANY doubt about the requirements, this is the approach I use. (And guess what? I seldom have clear requirements, even though I’m the person providing them for the code you see on this site. Requirements are hard…)

Notice that this class does not have an empty constructor. You must have a test before you instantiate the TestFirstClass.

TestFirstCode
  1. public class TestFirstCode : AgileCode
  2. {
  3.     /// <summary>
  4.     /// Use this to create Test First Code. Note there
  5.     /// is not empty constructor.
  6.     /// </summary>
  7.     /// <param name=”test”></param>
  8.     public TestFirstCode(UnitTest test)
  9.     {
  10.         UnitTest = test;
  11.     }
  12.     public override UnitTest UnitTest { get; set; }
  13.     public override string CodeText { get; set; }
  14. }

CONSIDER writing the tests before writing the code.

I find it interesting that this class is SO simple. The rules are intrinsic in the structure of the code. Now you’ll note that I say “Unit Test” when in reality it should be Collection<UnitTest>. I cannot remember a time when only one unit test covered all the bases.

In a very real sense, Test First Code is the only code that was never legacy code.

Now let’s look at the TestBase and UnitTest classes

Tests

TestBase

The test base class started out pretty simple, but then grew as I wrote tests. Passed is a pretty obvious property, but Cost, Benefit, and Value came from writing tests. I realized that I was interested in that information.

TestBase
  1. public abstract class TestBase
  2. {
  3.     public static double HourlyCost = 100.0;
  4.     public TestAuthorType AuthorType { get; set; }
  5.     public bool IsCompleted { get; set; }
  6.     public bool Passed { get; set; }
  7.     public abstract double Cost { get; }
  8.     public abstract double Benefit { get; }
  9.  
  10.     public double Value
  11.     {
  12.         get { return GetValue(Cost, Benefit); }
  13.     }
  14.  
  15.     internal static double GetValue(double cost, double benefit)
  16.     {
  17.         return benefit / cost;
  18.     }
  19. }

Most of the properties in this base class are self-evident, but I thought I would put it here so that you could see some of what is going on when we get to the unit test class.

UnitTest

So finally we are getting to the unit test. We have the needed context now.

UnitTest
  1. public class UnitTest : TestBase
  2. {
  3.     private static double _coverageTarget = 0.95;
  4.  
  5.     public UnitTest()
  6.     {
  7.         // Unit tests are written by the developer.
  8.         // When using TDD, the tests are written first.
  9.         AuthorType = TestAuthorType.Developer;
  10.     }
  11.  
  12.     private bool _isUICode;
  13.     /// <summary>
  14.     /// Gets or sets a value indicating whether the Code under
  15.     /// test is UI (User Interface) code.
  16.     /// </summary>
  17.     public bool IsUICode
  18.     {
  19.         get { return _isUICode; }
  20.         set
  21.         {
  22.             if (value)
  23.                 throw new InvalidOperationException(“UnitTests seldom work for UI layer, so minimize the code you write there.”);
  24.             else
  25.                 _isUICode = value;
  26.         }
  27.     }
  28.  
  29.     /// <summary>
  30.     /// Gets the value, in currency, for the cost of the test.
  31.     /// </summary>
  32.     public override double Cost
  33.     {
  34.         get { return Minutes * HourlyCost / 60.0; }
  35.     }
  36.  
  37.     /// <summary>
  38.     /// Gets or sets the minutes required to write the unit test.
  39.     /// </summary>
  40.     public double Minutes { get; set; }
  41.  
  42.     /// <summary>
  43.     /// Gets a value for the benefit of the test. This is a value
  44.     /// between 100 and 0.
  45.     /// </summary>
  46.     public override double Benefit
  47.     {
  48.         get
  49.         {
  50.             // Obviously this code seem silly (and hopefully
  51.             // the compiler is smart enough to take it out) but
  52.             // the point is that the tests adds value whether it
  53.             // passes or not.
  54.             if (Passed || !Passed)
  55.                 return GetBenefit(TotalCodeCoverage);
  56.             else
  57.                 return 0;
  58.         }
  59.     }
  60.  
  61.     internal static double GetBenefit(double codeCoverage)
  62.     {
  63.         if (codeCoverage > _coverageTarget)
  64.             return 100;
  65.         else
  66.             return 100.0 * codeCoverage / _coverageTarget;
  67.     }
  68.  
  69.     /// <summary>
  70.     /// Gets or sets the value for the code coverage of the
  71.     /// entire code base to which the code under test belongs.
  72.     /// </summary>
  73.     public double TotalCodeCoverage { get; set; }
  74.  
  75. }

The hourly cost was created to help determine the value.

While it might seem silly to say “Developers write unit tests.” if you’ve been around unit tests for many years, those new to unit tests hear the word test and think, “Oh… Testers write tests, developers only write code.”

Developers write unit tests.

So what is the value of a unit test. At the beginning of this post I pointed to the value of unit tests in refactoring. Let’s walk through some of the extremes to understand some of this value.

If I have a large legacy code base and I write one unit test, what is the value of that unit test? I’ll tell you, it is of no value. It will not help you with refactoring – there is so much untested code that you don’t dare use that solitary unit test as a basis for deleting code.

So when does a unit test become valuable? That’s not really possible to measure on one single unit test. The value is derived from having the maximum amount of code coverage. When you do that you suddenly move to a new level. Sort of like a sonic boom, your in a whole new world of high performance.

DO maximize your code coverage so you derive maximum value from your unit tests.

Let’s revisit the notion of writing unit tests on legacy code. One might argue that “I have to start somewhere, so I’ll write that first test, then the second, then someday I’ll be done.” That’s not the case. Without exception, when I tried to write unit tests for legacy code I discovered tight coupling in the code that made it hard to test. Mocking objects can help you some, but I found that writing unit tests for legacy code was one of the most frustrating exercises I’ve ever tried.

AVOID going back and writing tests on legacy code.

Misconceptions

DO NOT wait until lots of code is written then try to write your unit tests.

There are many misconceptions about unit testing that will make it a challenge for people to pick up. If you are going to write a bunch of code and then go back and write the tests IT WILL NEVER HAPPEN!

When you wait to write the tests you now view tests as a tax rather than as someone wise like Socrates.

Even with effective mocking strategies there are times you cannot achieve 100{f073afa9b3cad59b43edffc8236236232bb532d50165f68f2787a3c583ed137f} code coverage.

DO view untested agile code as a challenge to test (“I will find a way to test that”) but don’t lose any sleep if it really is untestable.

I did have one code base that I could not get above 98{f073afa9b3cad59b43edffc8236236232bb532d50165f68f2787a3c583ed137f}. But there are many times that someone told me something couldn’t be tested… There were quite surprised when I found a way.

Summary

I utilized the style in the book “Framework Design Guidelines” by Crzysztof Cwalina and Brad Abrams of DO, DO NOT, etc. in this summary of the points above.

  • DO NOT write legacy code.
  • DO minimize the code in your presentation tier.
  • DO write unit tests.
  • CONSIDER writing the tests before writing the code.
  • DO maximize your code coverage so you derive maximum value from your unit tests.
  • AVOID going back and writing tests on Legacy Code.
  • DO NOT wait until lots of code exists then try to write your unit tests.

I hope this captures some of the key reasons one should seriously consider making a lifestyle change for your coding practices so your code is “Fit for Life”.


Posted

in

,

by

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.