Skip to main content

.Net TDD (Test Driven Development) by example - Part 1


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)

At Assemblysoft we specialise in Custom Software Development tailored to your requirements. We have experience creating Booking solutions, as we did for HappyCamperVan Hire. You can read more here.

We can onboard and add value to your business rapidly. We are an experienced Full-stack development team able to provide specific technical expertise or manage your project requirements end to end. We specialise in the Microsoft cloud and .NET Solutions and Services. Our developers are Microsoft Certified. We have real-world experience developing .NET applications and Azure Services for a large array of business domains. If you would like some assistance with Azure | Azure DevOps Services | Blazor Development  or in need of custom software development, from an experienced development team in the United Kingdom, then please get in touch, we would love to add immediate value to your business.

Assemblysoft - Your Safe Pair of Hands

https://assemblysoft.com/


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
  1. There is a using statement for 'Microsoft.VisualStudio.TestTools.UnitTesting'.
  2. The 'UnitTest1' class is decorated with a [TestClass] attribute.
  3. 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 judgement 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

    /// <summary>
    /// Roman Numerals
    /// </summary>
    /// <remarks>
    /// There are seven symbols that can be used to write any roman numeral
    /// </remarks>
    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()
                        {
//... 1000
                            NumericValue = Convert.ToInt32(RomanNumeralsType.M),
                            RomanNumeralRepresentation = RomanNumeralsType.M.ToString()
                        },
                    new RomanNumeralPair()
                        {
//... 500
                            NumericValue = Convert.ToInt32(RomanNumeralsType.D),
                            RomanNumeralRepresentation = RomanNumeralsType.D.ToString()
                        },
                    new RomanNumeralPair()
                        {
//... 100
                            NumericValue = Convert.ToInt32(RomanNumeralsType.C),
                            RomanNumeralRepresentation = RomanNumeralsType.C.ToString()
                        },
                    new RomanNumeralPair()
                        {
//... 50
                            NumericValue = Convert.ToInt32(RomanNumeralsType.L),
                            RomanNumeralRepresentation = RomanNumeralsType.L.ToString()
                        },
                    new RomanNumeralPair()
                        {
//... 10
                            NumericValue = Convert.ToInt32(RomanNumeralsType.X),
                            RomanNumeralRepresentation = RomanNumeralsType.X.ToString()
                        },
                    new RomanNumeralPair()
                        {
//... 5
                            NumericValue = Convert.ToInt32(RomanNumeralsType.V),
                            RomanNumeralRepresentation = RomanNumeralsType.V.ToString()
                        },
                    new RomanNumeralPair()
                        {
//... 1
                            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();
            
            //iterate through the list, starting with the highest value
            foreach (var currentPair in _romanNumeralList)
            {
                while (number >= currentPair.NumericValue)
                {//...number is greater than or equal to the current value so store the roman numeral and decrement it's value 
                    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

Also checkout some other musings via my blazor.net and azure blog here carlrandall.net

Revision History

11th March 2013 - First revision with code.

Popular posts from this blog

Windows Azure Storage Emulator failed to install

CodeProject Windows Azure Storage Emulator failed to install When attempting to install a new version of the Azure Storage Emulator either as a separate installation package or automatically as part of an Azure SDK update, you may run into an error message which states the storage emulator has failed to install. This can occur using the Web Platform Installer (WebPI), NuGet Package Manager or when performing the install manually. Below is the message received using the WebPI.   Storage Emulator Background  (optional reading) The windows azure storage emulator executable lives under the Microsoft SDKs directory as shown below: Configuration If we take a quick look inside the WAStorageEmulator.exe.config file we can see each of the storage services pointing to local service endpoints. <StorageEmulatorConfig>     <services>       <service name=" Blob " url="http://127.0.0.1:10000/"/>       <service

Debugging Python and Iron Python using Visual Studio

Now Python is a first class citizen since the release of Visual Studio 2017 and can be configured directly from the Installation IDE, below are a few settings worth bookmarking for your next python integration project. Debugging Python One of the first things you are going to want to do is step through your code when using Visual Studio, particularly as the language is dynamic and inspection of local and global scope soon becomes necessary. One thing to note is that if you start from a native python project, this is all wired up for you but if you are using .Net to call python modules or want to support an older python version, such as 2.7, you will soon see that breakpoints are not being hit due to symbols not being loaded.   Enable Just My Code To distinguish user code from non-user code in .net, Just My Code looks at two things: PDB (Program Database) files, and Optimization Program Database A .pdb file, otherwise known as a symbol file, maps the identifiers

Azure DevOps Authorisation

Managing whether an identity has access to a given  service, feature, function, object, or method in Azure DevOps comes down to authorisation. Fortunately, by default, the DevOps permissions are set in such a way to enable you to focus on the job at hand, DevOps. Loosely translated this means 'don't get in my way'. My experience is that the Azure DevOps team have done a good job at this, enabling you to crack on developing, building, testing and releasing without much hindrance. Working with relaxed permissions is great when you are the owner and possibly either a one man band or small team but as soon as we need to consider larger teams, varying roles with approvals and degrees of access, authorisation becomes a real concern. I was recently involved in a project utilising offshore developers where trust was a concern and a number of specific teams handling specific roles needed to come together to approve a set of pipelines.  This article is a pick of findings a