Introduction
In part 1 of this mini-series, we will develop a trivial business logic layer from scratch with a TDD approach with the goal of achieving the following:
- Better code quality through Red, Green, Refactor
- Documentation that grows as we develop and remains up to date
- Automatic regression test harness
This will primarily involve creating unit tests first, having them fail,
making them pass and then refactoring the code to be of better quality and then re-running the tests.
When using tools such as resharper to aid
in refactoring code, having the tests in place right from the beginning really gives
you peace of mind that you haven't broken anything. It also helps the thought processes while designing and developing an application or feature to be more targeted.
We will further develop the application in part 2 to add an MVC4 web client and continue the TDD story...
Some Background
Test First or Test Driven development is a valuable software engineering practice.
It comprises of much more than this article could attempt to cover such as Acceptance Test Driven Development (ATDD) and Behaviour driven development (BDD). We will focus on a subset of TDD that encourages developer testing and aids tremendously
in shipping software rather than traditionally having testing as a secondary phase or responsibility of a tester and promotes testing as a first class
citizen in our everyday software development lifecycle (SDLC).
How many times have you intended to write unit tests after a feature has been created and due to time constraints ended up leaving it and moving
onto the next part of the application with an uncertain feeling that it would have been better to have them in place before adding more complex layers?
Following a TDD approach eliminates this as the tests are the first thing to consider as part of an initial implementation.
Development Costs
Fixing bugs after software is shipped has been proven to be much more expensive than having unit tests in place that can be run each time code
is about to be checked into source control for minor or substantial changes to the system. Unit testing is also cheaper than Integration testing,
System testing and Functional testing. Although not a direct replacement for any of the above, having a large set of unit tests in place gives piece of
mind that each days development has been a cost effective one and the code is still in good shape.
Sample Code
The sample code consists of a library that will return a string Roman numeral representation when passed an integer between 1 and 3000.
The sample code, written using Visual Studio 2012, is deliberately left simple to allow focus on development style rather than getting side tracked with
implementation details. It consists of two C# class libraries. The first contains an MS Test class library which will contain our unit tests and the second is a standard
class library which we will use to develop the functionality. The unit test project class library is kept separate to ensure we are only testing the public parts
of our business logic, without exposing internals to the tests, which are likely to change over time.
The Tools
There are numerous unit testing frameworks available. This article uses the one out of the box with Visual Studio, MS Test.
Using NUnit, one of my favourites, or other frameworks would work also and is down to personal preference
or the infrastructure you work in. Using MS Test helps the article code to run with no other dependencies.
- Visual Studio 2012
- MS Test (included as part of Visual Studio 2012)
Creating the libraries
Because we are working with a TDD approach, it has forced me to think a little ahead of time, even before naming my project. I know I want to provide a library for roman numerals
but if I want to test it, the more loosely coupled I keep it, the more it will remain testable. As a result I will name the solution 'AssemblySoft.NumberSystems' to support
future number system conversions. Choosing the 'Blank Solution' from Visual studio will get us started as shown below:
Next we will add the Unit Test project as show below:
Lastly for this step, we will create the class library to hold our business logic under test as shown below:
We can go ahead and perform the following:
- Delete the default classes created for us in the two projects
- Create a new Unit Test as shown and build
If you open the new 'UnitTest1.cs' class file you will be presented with the following:
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NumberSystemConverter_UnitTests
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
}
}
}
As you can see, it is just a standard class with three noticeable differences
- There is a
using
statement for 'Microsoft.VisualStudio.TestTools.UnitTesting
'. - The '
UnitTest1
' class is decorated with a [TestClass]
attribute. - The '
TestMethod1
' method is decorated with a [TestMethod]
attribute.
What this tells us is that a reference assembly has been added to our test project and if we look it is named 'Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll'.
It further uses this reference with the using namespace and adds attributes to both the class and method, allowing it to know what classes should be used
for testing runs and which classes just act as helpers or are just our own code.
In TDD, we are encouraged to first make our test fail with as minimal code as possible. As you get more familiar with the process you can use some reasonable
judgment on this and my particular take is to create the minimal piece of usable code as a first step and ensure the test fails. Now you could approach this with
a method that does not have a body and just throws a 'NotImplemented
' exception or you could think of a valid exception when consumers start using your library to kick things off.
Going Back to the Requirements
My loose requirements dictate that I can get a roman numeral for numbers between 1 and 3000. So here we could also say that entering numbers less than 1 and greater
than 3000 should fail both the library but more importantly at this stage the test as we are working in test first. Now although we haven't written
a line of meaningful code yet, hopefully, already the thinking surrounding test first is starting to have an effect.
Our First Two Tests - RED
We will leave the existing test method in place and in the code editor, add two new tests, following the same attribute guidelines and the results will be as follows:
[TestMethod]
[ExpectedException(typeof(IndexOutOfRangeException))]
public void Number_Greater_Than_ThreeThousand_Throws_IndexOutOfRangeException_TestMethod()
{
var converter = new RomanNumeralConverter();
converter.ConvertRomanNumeral(3001);
}
[TestMethod]
[ExpectedException(typeof(IndexOutOfRangeException))]
public void Number_Less_Than_One_Throws_IndexOutOfRangeException_TestMethod()
{
var converter = new RomanNumeralConverter();
converter.ConvertRomanNumeral(-1);
}
So after adding the above code you are maybe concerned that you don't have a 'RomanNumeralConverter
' with a 'ConvertRomanNumeral
' method and your code doesn't build.
That's OK, it's expected.
Get the Tests Failing with the Minimal Amount of (useful) Code
We can use visual studio to stub out the class and method declarations for us, all within the test project for now, by using the context menu when selecting
the 'RomanNumeralConverter
' and selecting 'Generate class for RomanNumeralConverter
' as shown below:
Do the same for the method definition as well.
At this stage we have a new .cs file in our test project named 'RomanNumeralConverter.cs' with a class and method definition with no useful implementation.
The test methods are now satisfied and the project builds.
The new class is shown below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NumberSystemConverter_UnitTests
{
class RomanNumeralConverter
{
internal void ConvertRomanNumeral(int p)
{
throw new NotImplementedException();
}
}
}
Notice the 'NotImplemented' exception in the body of the method. Going back to our test methods, we added another
attribute '[ExpectedException(typeof(IndexOutOfRangeException))]' to each method that states we are expecting an IndexOutOfRange
exception, not a NotImplementedException
. If we run our tests now, we should be in that all important first state of 'Fail'
or sometimes referred to as 'Red' (from our article heading image).
Using the Test Explorer to View and Run our Tests
Now are tests are in place, let's run the tests by right clicking on the test project and selecting 'Run Tests' as shown below:
This will start the MS Test runner and show the Test Explorer window as shown below:
The Test Explorer allows drilling down into individual tests and aids in diagnosing the reason for a test failure. The screenshot above highlights the exception that
was thrown by the 'ConvertRomanNumeral
' method. Have a play with the test explorer and get familiar with it. You can run tests in different ways also, you will find what works best for you.
One thing we can go ahead and do is delete the 'TestMethod
' that
Visual Studio added as this is passing our test run which is not what we want for the 'red' phase.
We are now ready to move to the next phase which is to pass the test or make it 'Green'
Make the Test Pass (Green)
Our two methods should fail if the number passed into the 'ConvertRomanNumeral
' method are less than 1 or greater than 3000. Our tests are expecting
an IndexOutOfRangeExcpetion
in these cases so let's throw one of those from the method instead if the number supplied is indeed <1 || > 3000.
The amended code is shown below:
internal void ConvertRomanNumeral(int p)
{
if (p < 1 || p > 3000)
{
throw new IndexOutOfRangeException("The number supplied is out of the expected range (1 - 3000).");
}
}
If we run the two tests again, we will see the tests indeed passing as expected. (Green)
One thing to note at this point is that it is generally good practice to perform the test as a single statement, rather than having branching logic inside the test,
this really aids in ensuring the tests are still valid and also deterministic each time you run them. In this case the test is always expecting one thing to happen, an
IndexOutOfRange
exception.
Refactor
Refactoring is an important step in the TDD lifecycle. When we refactor we are making changes to the internals of our business logic without affecting the public
API that our tests are consuming. This step may involve a complete re-write of our implementation, renaming variables, adding further abstractions or design patterns.
The important thing to remember though is our test library is a client or consumer of our business library as are other clients. Not changing the public
external functionality or API will ensure our tests will still validate our system under test, regardless of internals. In fact this acts as a great
way to ensure that when we use tools such as resharper to make our code smarter, we can quickly determine if anything has broken by simply re-running the tests.
Our first stab at making something work when designing and developing a new feature is most often quite different looking than our final code that has been reviewed,
sanity checked and is considered a final implementation. The refactor step encourages this in that you can leave the process of housekeeping and as the term indicates,
'refactoring', as a phase in the lifecycle. This really helps with not getting too bogged down trying to create a perfect set of code first time round but rather focus
on the design of the API or public interface and on what you want to test, leaving refactoring to this point in the lifecycle.
It gives you a warm feeling to know it's OK to produce a rough first implementation which may contain stubs or whatever it takes to satisfy
the API at first. If of course in reality you can craft a useful first implementation in a reasonable time frame as your first cut, then go for it.
These are guidelines and should be adopted to suit making you productive but at the same time create a testable system.
Make Some Changes
At this point we can get things tidied up, work some more on the implementation and add features, all with the safe knowledge that we can test (at least
two scenarios) after making changes.
Let's do the following to the 'NumberSystemConverter' project:
- Move the '
RomanNumeralConverter
' class into the 'NumberSystemConverter' project - Change the namespace for '
RomanNumeralConverter
' class to 'NumberSystemConverter
' - Add the class access modifier to 'public' for the '
RomanNumeralConverter
' class - Set the access modifier for '
ConvertRomanNumeral
' to 'public' - Change the name of the parameter from 'p' to 'number' inside '
ConvertRomanNumeral'
- Change the return type to string and return an empty string below the if statement block
- Ensure the project builds
The code for the NumberSystemConverter
should be as follows:
using System;
namespace NumberSystemConverter
{
public class RomanNumeralConverter
{
public string ConvertRomanNumeral(int p)
{
if (p < 1 || p > 3000)
{
throw new IndexOutOfRangeException(
"The number supplied is out of the expected range (1 - 3000).");
}
return string.Empty;
}
}
}
Let's do the following to the 'NumberSystemConverter_UnitTests' project:
- Remove the '
RomanNumeralConverter
' class from the test project - Add a reference to the 'NumberSystemConverter' project to the test class
- Add a
using
statement to the 'UnitTest1.cs' file to 'NumberSystemConverter'
- Rename the '
UnitTest1
' class to 'RomanNumeralConverterUpperAndLowerBoundsUnitTests'
- Rename the .cs file the same
- Ensure the project builds
- Run the tests and ensure the results are the same (Fix any bugs introduced in this step if not)
The code for the NumberSystemConverter_UnitTests should be as follows:
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NumberSystemConverter;
namespace NumberSystemConverter_Tests
{
[TestClass]
public class RomanNumeralConverterUpperAndLowerBoundsUnitTests
{
[TestMethod]
[ExpectedException(typeof (IndexOutOfRangeException))]
public void Number_Greater_Than_ThreeThousand_Throws_IndexOutOfRangeException_TestMethod()
{
var converter = new RomanNumeralConverter();
converter.ConvertRomanNumeral(3001);
}
[TestMethod]
[ExpectedException(typeof (IndexOutOfRangeException))]
public void Number_Less_Than_One_Throws_IndexOutOfRangeException_TestMethod()
{
var converter = new RomanNumeralConverter();
converter.ConvertRomanNumeral(-1);
}
[TestMethod]
[ExpectedException(typeof (IndexOutOfRangeException))]
public void Number_Zero_Throws_IndexOutOfRangeException_TestMethod()
{
var converter = new RomanNumeralConverter();
converter.ConvertRomanNumeral(0);
}
}
}
After some minor refactoring, we are able to still satisfy that our tests are still passing as they did before.
Introduce More Tests (Red)
Let's add a few more tests for our 'ConvertRomanNumeral
' method and also flesh out its implementation some more to cater for the numbers 1 - 3000.
Let's do the following to the 'NumberSystemConverter_UnitTests' project:
- Add a new TestClass in the same file named 'RomanNumeralConverterExpectedValuesUnitTests'
- Add method: Number_Equal_One_Expected_Result_I_TestMethod
- Add method: Number_Equal_ThreeThousand_Expected_Result_MMM_TestMethod
- Add method: Number_Equal_55_Expected_Result_LV_TestMethod
- Add method: Number_Equal_100_Expected_Result_C_TestMethod
- Add method: Number_Equal_500_Expected_Result_D_TestMethod
- Add method: Number_Equal_599_Expected_Result_DLXXXXVIIII_TestMethod
- Add method: Number_Equal_2013_Expected_Result_MMXIII_TestMethod
The code for the new methods is shown below:
[TestClass]
public class RomanNumeralConverterExpectedValuesUnitTests
{
[TestMethod]
public void Number_Equal_One_Expected_Result_I_TestMethod()
{
var converter = new RomanNumeralConverter();
var result = converter.ConvertRomanNumeral(1);
Assert.AreEqual(result, "I");
}
[TestMethod]
public void Number_Equal_ThreeThousand_Expected_Result_MMM_TestMethod()
{
var converter = new RomanNumeralConverter();
var result = converter.ConvertRomanNumeral(3000);
Assert.AreEqual(result, "MMM");
}
[TestMethod]
public void Number_Equal_55_Expected_Result_LV_TestMethod()
{
var converter = new RomanNumeralConverter();
var result = converter.ConvertRomanNumeral(55);
Assert.AreEqual(result, "LV");
}
[TestMethod]
public void Number_Equal_100_Expected_Result_C_TestMethod()
{
var converter = new RomanNumeralConverter();
var result = converter.ConvertRomanNumeral(100);
Assert.AreEqual(result, "C");
}
[TestMethod]
public void Number_Equal_500_Expected_Result_D_TestMethod()
{
var converter = new RomanNumeralConverter();
var result = converter.ConvertRomanNumeral(500);
Assert.AreEqual(result, "D");
}
[TestMethod]
public void Number_Equal_599_Expected_Result_DLXXXXVIIII_TestMethod()
{
var converter = new RomanNumeralConverter();
var result = converter.ConvertRomanNumeral(599);
Assert.AreEqual(result, "DLXXXXVIIII");
}
[TestMethod]
public void Number_Equal_2013_Expected_Result_MMXIII_TestMethod()
{
var converter = new RomanNumeralConverter();
var result = converter.ConvertRomanNumeral(2013);
Assert.AreEqual(result, "MMXIII");
}
}
There are a few things worth noting about the methods. Their names are descriptive of their purpose. This makes it easy to understand what the purpose of the test is.
Secondly by adding classes for specific types of test, we have been able to isolate bounds checking tests from those that are for specific numeric values.
This also aids dramatically further down the road when you need to come back and find the tests that you're new fix or feature my well impact.
You will notice the heavy use of the 'Assert' keyword for comparing the results from the majority of tests in this section. This is a common practice
but it should be stated that this is only the tip of the iceberg in terms of options on how to determine and compare results. Each framework has a multitude
of different options, some dealing with collections, strings, bools, and other types. Needless to say, this is a learning adventure and each time you
adhere to and grow your understanding of this way of developing new features, your arsenal of best ways to test a value will also increase.
We should now be able to run our new tests and indeed see them fail as our 'ConvertRomanNumeral
' method will in all but two cases return an empty string.
This can be seen below:
Well that was what we expected and we can also see that the return value is indeed an empty string inside the expected result for the test.
You will notice as before that each test is attempting to evaluate a single statement, in this case with a single dedicated Assert.
Make the Test Pass (a second time) (Green)
We will add some implementation to the 'ConvertRomanNumeral
' method and some supporting data to aid in providing passes for the new tests.
The first thing we will do is add some supporting types as shown below:
#region Supporting Types
enum RomanNumeralsType
{
M = 1000,
D = 500,
C = 100,
L = 50,
X = 10,
V = 5,
I = 1
}
internal class RomanNumeralPair
{
public int NumericValue { get; set; }
public string RomanNumeralRepresentation { get; set; }
}
#endregion
As roman numerals are only made up of seven different symbols, this should suffice.
The next part is to use the data to create an in memory list of number / roman numeral pairs so that we can later use it inside our 'ConvertRomanNumeral
' method. This is shown below:
private readonly List<RomanNumeralPair> _romanNumeralList;
public RomanNumeralConverter()
{
_romanNumeralList = new List<RomanNumeralPair>()
{
new RomanNumeralPair()
{
NumericValue = Convert.ToInt32(RomanNumeralsType.M),
RomanNumeralRepresentation = RomanNumeralsType.M.ToString()
},
new RomanNumeralPair()
{
NumericValue = Convert.ToInt32(RomanNumeralsType.D),
RomanNumeralRepresentation = RomanNumeralsType.D.ToString()
},
new RomanNumeralPair()
{
NumericValue = Convert.ToInt32(RomanNumeralsType.C),
RomanNumeralRepresentation = RomanNumeralsType.C.ToString()
},
new RomanNumeralPair()
{
NumericValue = Convert.ToInt32(RomanNumeralsType.L),
RomanNumeralRepresentation = RomanNumeralsType.L.ToString()
},
new RomanNumeralPair()
{
NumericValue = Convert.ToInt32(RomanNumeralsType.X),
RomanNumeralRepresentation = RomanNumeralsType.X.ToString()
},
new RomanNumeralPair()
{
NumericValue = Convert.ToInt32(RomanNumeralsType.V),
RomanNumeralRepresentation = RomanNumeralsType.V.ToString()
},
new RomanNumeralPair()
{
NumericValue = Convert.ToInt32(RomanNumeralsType.I),
RomanNumeralRepresentation = RomanNumeralsType.I.ToString()
}
};
Now we have a lookup, it's time to attempt to use it (newly added code in bold) as shown below:
public string ConvertRomanNumeral(int number)
{
if (number < 1 || number > 3000)
{
throw new IndexOutOfRangeException("The number provided is outside the expected range ( 1 - 3000)");
}
var builder = new StringBuilder();
foreach (var currentPair in _romanNumeralList)
{
while (number >= currentPair.NumericValue)
{
builder.Append(currentPair.RomanNumeralRepresentation);
number -= currentPair.NumericValue;
}
}
return builder.ToString();
}
After adding some new implementation as part of the green phase, we are now able to perform our next round of testing.
Debugging Tests
One thing worth mentioning at this point is that sometimes you will write code that you are convinced will pass the test but doesn't,
usually because of something you have overlooked, at least that's what happens in my case
.
So the thing to do here as you would normally is to crack open the debugger. In our example here it fits quite well as it is generally more difficult
to debug two class libraries without some kind of client or without attaching a debugger explicitly. Fortunately in this case with using the MS Test Framework which is already baked
into Visual Studio, using the debugger to debug a test is as easy as right clicking on the specific test in test explorer, or right clicking while in the test method
in the Visual Studio code editor and selecting 'debug tests' This will automatically attach the debugger in context where you can step through normally.
So the results of the last implementation and test run can be seen below:
Refactor (round two)
At this stage I went ahead and tidied up some more, moved some logic into a lookup, added some comments and did a round of refactoring. I would recommend
using resharper or another code quality tool, make your changes, run the tests again, make sure your happy and get the code checked in for the day.
Conclusion
We have really only touched on the TDD story but hopefully some of you find it useful, especially if not familiar with Red, Green Refactor.
Creating unit tests as a first step in developing new features has many benefits. Once the tests are in place, you have a live set of documentation
which not only demonstrates the requirements but helps enforce them every day you develop, acting as an automatic regression test harness. It may at
first seem like more work than just focusing on an implementation detail but as you adopt these kinds of approaches it lends a hand in both
the way you think about a problem and also acts as a safeguard for every new change that gets made to the code base.
The next article will take this approach further, looking at the client testing challenges as well as applying similar techniques to existing legacy code (brownfield).
The areas we looked at
- Red, Green, Refactor - Fail, Pass, Change
- Well structured code - the process encourages more decoupled design
- Self documenting - start with a specification and then enforce it
- Automatic regression test harness - if someone breaks something, the tests will scream
Revision History
- 11 March 2013 - First revision with code.