Click here to Skip to main content
16,020,568 members
Articles / Programming Languages / C#

Test Driven / First Development by Example

Rate me:
Please Sign up or sign in to vote.
4.79/5 (30 votes)
5 Apr 2007BSD7 min read 133.8K   774   103   19
A step by step example of how to build up an algorithm by writing the unit tests first

Introduction

A lot has been written on the subject of test driven development, and especially on the idea that tests ought to be written first. This is an ideal for which I strive. However, I have a tendency to write the unit tests afterwards.

Some people learn better by example. This article, rather than going into great length about the principles of test driven development, will walk the reader through the process of building and testing an algorithm by writing the tests first, then changing the method being tested so that it fulfills the tests.

The final code and all the unit tests can be found in the accompanying download. This will require NUnit and Visual Studio 2005. The use of NUnit in this example is purely incidental, this example could just have easily used the unit test framework fin Visual Studio Team System or any number of other unit test frameworks. The example shows how to build a suite of tests rather than how to use a unit test framework. The reader is expected to understand the basics of using a unit test framework.

The Specimen Problem

I once saw a demo of how to create unit tests up front for a simple method. The method took a positive integer and turned it into Roman numerals. So, I'm going to do something similar. I'm going to take an integer and turn it into words, in English. The rules for this may change depending on the language, so if English is not your only language, you may like to try to repeat this exercise in another language.

So, if the integer is 1, the result will be "one". If the integer is 23, the result will be "twenty three" and so on. Also note, I'll be using British or Commonwealth English. So, for 101, the result in words is "one hundred and one". In American English, it would be "one hundred one".

The Walk Through

The algorithm will also be refined through refactoring techniques. Agile development methodologies, especially eXtreme Programming, suggests that you do the simplest thing possible to get the thing working. So, going by this premise, I'll work on the solution in bits. First, get it to return "one", then "one" or "two" depending on the input, and so on. Once 21 is reached, it should become obvious where some refactoring can take place and so on. The final solution will work for 32 bit integers only.

Getting Started

Visual Studio 2005 has some features that can help with writing the tests first. A test can be written that calls into the class under test and the smart tags will prompt you with an offer to create the message stub for you.

Screenshot - Creating method stubs

The stub looks like this:

C#
public static string NumberToEnglish(int p)
{
  throw new Exception("The method or operation is not implemented.");
}

If the test is completed to look like this:

C#
[Test]
public void NumberToEnglishShouldReturnOne()
{
  string actual = English.NumberToEnglish(1);
  Assert.AreEqual("one", actual, "Expected the result to be \"one\"");
}

The test should fail because the stub throws an exception, rather than do what the test expects.

NUnit reports the error like this:

test
"NumbersInWords.Test.EnglishTest.NumberToEnglishShouldReturnOne : 
    System.Exception : 
The method or operation is not implemented.".

The next thing to do is to ensure that the code satisfies the demands of the unit test. Agile methodologies, such as XP, say that only the simplest change should be made to satisfy the current requirements. In that case, the method being tested will be changed to look like this:

C#
public static string NumberToEnglish(int number)
{
  return "one";
}

At this point, the unit tests are re-run and they all work out.

Test "two"

Since the overall requirement is that any integer should be translatable into words, the next test should test that 2 can be translated. The test looks like this:

C#
[Test]
public void NumberToEnglishShouldReturnTwo()
{
  string actual = English.NumberToEnglish(2);
  Assert.AreEqual("two", actual, "Expected the result to be \"two\"");
}

However, since the method being tested returns "one" regardless of input at this point, the test fails:

NumbersInWords.Test.EnglishTest.NumberToEnglishShouldReturnTwo :
Expected the result to be "two"
    String lengths are both 3.
    Strings differ at index 0.
    expected: <"two">
     but was: <"one">
    ------------^

Again, keeping with the simplest change principle, the code is updated to look like this:

C#
public static string NumberToEnglish(int number)
{
  if (number == 1)
    return "one";
  else
    return "two";
}

The tests now pass.

Test "three" to "twenty"

A third test can now be written. It tests for an input of 3 and an expected return of "three". Naturally, at this point, the test fails. The code is updated again and now looks like this:

C#
public static string NumberToEnglish(int number)
{
  switch (number)
  {
    case 1:
      return "one";
    case 2:
      return "two";
    default:
      return "three";
  }
}

To cut things short, the new tests and counter-updates continue like this until the numbers 1 to 20 can be handled. The code will eventually look like this:

C#
public static string NumberToEnglish(int number)
{
  switch (number)
  {
    case 1:
      return "one";
    case 2:
      return "two";
    case 3:
      return "three";
    case 4:
      return "four";
    case 5:
      return "five";
    case 6:
      return "six";
    case 7:
      return "seven";
    case 8:
      return "eight";
    case 9:
      return "nine";
    case 10:
      return "ten";
    case 11:
      return "eleven";
    case 12:
      return "twelve";
    case 13:
      return "thirteen";
    case 14:
      return "fourteen";
    case 15:
      return "fifteen";
    case 16:
      return "sixteen";
    case 17:
      return "seventeen";
    case 18:
      return "eighteen";
    case 19:
      return "nineteen";
    default:
      return "twenty";
  }
}

Test "twenty one" to "twenty nine"

At this point, it looks like it will be pretty easy to do 21, but a pattern is about to emerge. After the tests for 21 and 22 have been written, the code is refactored to look like this:

C#
public static string NumberToEnglish(int number)
{
  if (number < 20)
    return TranslateOneToNineteen(number);
  if (number == 20)
    return "twenty";
  return string.Concat("twenty ", TranslateOneToNineteen(number - 20));
}

private static string TranslateOneToNineteen(int number)
{
  switch (number)
  {
    case 1:
      return "one";
    case 2:
      return "two";
    case 3:
      return "three";
    case 4:
      return "four";
    case 5:
      return "five";
    case 6:
      return "six";
    case 7:
      return "seven";
    case 8:
      return "eight";
    case 9:
      return "nine";
    case 10:
      return "ten";
    case 11:
      return "eleven";
    case 12:
      return "twelve";
    case 13:
      return "thirteen";
    case 14:
      return "fourteen";
    case 15:
      return "fifteen";
    case 16:
      return "sixteen";
    case 17:
      return "seventeen";
    case 18:
      return "eighteen";
    default:
      return "nineteen";
  }
}

Now all the tests from 1 to 22 pass. 23 to 29 can be assumed to work because it is using well tested logic.

Test "thirty" to "thirty nine"

30 is a different story. The test will fail like this:

NumbersInWords.Test.EnglishTest.NumberToEnglishShouldReturnThirty :
Expected the result to be "thirty"
    String lengths differ.  Expected length=6, but was length=10.
    Strings differ at index 1.
    expected: <"thirty">
     but was: <"twenty ten">

    -------------^

By using the principle of doing the simplest thing that will work, the public method changes to:

C#
public static string NumberToEnglish(int number)
{
  if (number < 20)
    return TranslateOneToNineteen(number);
  if (number == 20)
    return "twenty";
  if (number <= 29)
    return string.Concat("twenty ", TranslateOneToNineteen(number - 20));
  return "thirty";
}

Naturally, the test for 31 will fail:

NumbersInWords.Test.EnglishTest.NumberToEnglishShouldReturnThirtyOne :
Expected the result to be "thirty one"
    String lengths differ.  Expected length=10, but was length=6.
    Strings differ at index 6.
    expected: <"thirty one">

     but was: <"thirty">
    ------------------^

So the code is changed again. This time to:

C#
public static string NumberToEnglish(int number)
{
  if (number < 20)
    return TranslateOneToNineteen(number);
  if (number == 20)
    return "twenty";
  if (number <= 29)
    return string.Concat("twenty ", TranslateOneToNineteen(number - 20));
  if (number == 30)
    return "thirty";
  return string.Concat("thirty ", TranslateOneToNineteen(number - 30));
}

Test "forty" to "ninety nine"

A test for 40 will fail:

NumbersInWords.Test.EnglishTest.NumberToEnglishShouldReturnForty :
Expected the result to be "forty"
    String lengths differ.  Expected length=5, but was length=10.
    Strings differ at index 0.
    expected: <"forty">
     but was: <"thirty ten">
    ------------^

The necessary code change starts to draw out a pattern. Of course, the pattern could have been quite easily predicted, but since this code is being built by the simplest change only rule, the pattern has to emerge before it can be acted upon.

The pattern repeats itself until it gets to 99. By this point, the public method looks like this:

C#
public static string NumberToEnglish(int number)
{
  if (number < 20)
    return TranslateOneToNineteen(number);
  int units = number % 10;
  int tens = number / 10;
  string result = "";
  switch (tens)
  {
    case 2:
      result = "twenty";
      break;
    case 3:
      result = "thirty";
      break;
    case 4:
      result = "forty";
      break;
    case 5:
      result = "fifty";
      break;
    case 6:
      result = "sixty";
      break;
    case 7:
      result = "seventy";
      break;
    case 8:
      result = "eighty";
      break;
    default:
      result = "ninety";
      break;
  }
  if (units != 0)
    result = string.Concat(result, " ", TranslateOneToNineteen(units));
  return result;
}

Test "one hundred"

The test for 100 will fail. The failure message is:

NumbersInWords.Test.EnglishTest.NumberToEnglishShouldReturnOneHundred :
Expected the result to be "one hundred"
    String lengths differ.  Expected length=11, but was length=6.
    Strings differ at index 0.
    expected: <"one hundred">
     but was: <"ninety">
    ------------^

A quick change to the public method allows the test to pass:

C#
public static string NumberToEnglish(int number)
{
  if (number == 100)
    return "one hundred";

  if (number < 20)
    return TranslateOneToNineteen(number);
  // Remainder omitted for brevity
}

Test "one hundred and one" to "one hundred and ninety nine"

What about 101? That test fails like this:

NumbersInWords.Test.EnglishTest.NumberToEnglishShouldReturnOneHundredAndOne :
Expected the result to be "one hundred and one"
    String lengths differ.  Expected length=19, but was length=10.
    Strings differ at index 0.
    expected: <"one hundred and one">
     but was: <"ninety one">
    ------------^

At this point, it should be easy to see that some of the work that has been done previously can be re-used with a small amount of refactoring. First, refactor most of the body of the public method into a class called TranslateOneToNinetyNine. Then, re-test to ensure that the refactoring process hasn't introduced any new problems.

In Visual Studio 2005, it is very easy to highlight some code and extract it into a new method, thus allowing it to be reused by being called from multiple places.

Screenshot - Extract Method

Now the public method looks like the following and all previously successful tests continue to be successful.

C#
public static string NumberToEnglish(int number)
{
  if (number == 100)
    return "one hundred";

  return TranslateOneToNinetyNine(number);
}

For numbers from 101 to 199, the pattern is "one hundred and X" where X is the result of the translation between 1 and 99. Because it would take too long to write all those tests, it is possible to write just the edge cases and one or two samples from the middle of the range. That should give enough confidence to continue onwards. In this case, the tests are for 101, 115, 155 and 199.

The code is then re-written to support those tests:

C#
public static string NumberToEnglish(int number)
{
  if (number < 100)
    return TranslateOneToNinetyNine(number);
  if (number == 100)
    return "one hundred";

  string result = string.Concat("one hundred and ",
    TranslateOneToNinetyNine(number - 100));

  return result;
}

Test "two hundred"

From this point onwards, the pattern of what needs to be done should be obvious. For that reason, the details of the intermediate steps are skipped until all positive integers can be written. If, however, you wish to read about these intermediate steps, then you can read the unabridged version of this article on my website.

The limitations of an integer (Int32) mean that this section reaches the upper limits of 2147483647. Unless an Int64 is used, there is no continuation to the trillion range.

Final Stages

To this point, all positive integers are successfully being translated from an integer into a string of words. At this point, through code reuse, it should be a fairly simple matter to refactor the code to work with negative numbers and zero.

Zero is easy enough. The unit test is put in place:

C#
[Test]
public void NumberToEnglishShouldReturnZero()
{
  string actual = English.NumberToEnglish(0);
  Assert.AreEqual("zero", actual, "Expected the result to be \"zero\"");
}

And it promptly fails because there is no code to support it. The public method is changed so that it does a quick check at the start:

C#
public static string NumberToEnglish(int number)
{
  if (number == 0)
      return "zero";
  if (number < 1000000000)
      return TranslateOneToNineHundredAndNintyNineMillion...(number);

  int billions = number / 1000000000;
  string result = string.Concat(TranslateOneToNineteen(billions), " billion");

  int remainder = number % 1000000000;
  if (remainder == 0)
    return result;

  if (remainder < 100)
    return string.Concat(result, " and ", TranslateOneToNinetyNine(remainder));

  return string.Concat(result, " ",
    TranslateOneToNineHundredAndNintyNineMillion...(remainder));
}

Now the test passes. Next is to permit negative numbers.

C#
[Test]
public void NumberToEnglishShouldReturnNegativeOne()
{
  string actual = English.NumberToEnglish(-1);
  Assert.AreEqual("negative one", actual, "Expected the result to be 
    \"negative one\"");
}

This fails, so the code is refactored to this:

C#
public static string NumberToEnglish(int number)
{
  if (number == 0)
    return "zero";

  string result = "";
  if (number < 0)
    result = "negative ";

  int absNumber = Math.Abs(number);

  return string.Concat(result,
    TranslateOneToTwoBillion...SixHundredAndFortySeven(absNumber));
  // Method call above contracted for brevity
}

And the test passes. But what about that final edge case? int.MinValue? The test is written, but it fails. The reason is that Math.Abs(int.MinValue) isn't possible. So, as this is a one off case, the easiest solution is to put in a special case into the public method:

C#
// Special case for int.MinValue.
if (number == int.MinValue)
  return "negative two billion one hundred and forty seven million " +
    "four hundred and eighty three thousand six hundred and forty eight";

Conclusion

This article demonstrates how to unit test and build a piece of code in small increments. The tests continually prove that the developer is on the right path. As the code is built, the constant rerunning of existing tests prove that any new enhancements do not break existing code. If, for any reason, it is later found that the code contains a bug, a test can easily be created that exercises that incorrect code and a fix is produced.

The full final code is available in the associated download along with a set of NUnit tests.

History

  • Version 1.0 of the article.

    Note: This is the abridged version of the article. For the full article, visit here.

  • Version 1.01

    A minor update regarding the expectations of the reader in the introduction.

License

This article, along with any associated source code and files, is licensed under The BSD License


Written By
Technical Lead
Scotland Scotland
Have been a Code Project MVP 5 years running and was Microsoft C# MVP 4 years running, MBCS, MIAP and a whole bunch of other stuff. Now I just help run Scottish Developers which is a user group with software development events in Edinburgh, Glasgow and Dundee and I have also started an open source project to help with Password Validation

Main topics I blog about:
* Parallelization in .NET
* Code Quality
* Data Security

Comments and Discussions

 
GeneralMy vote of 5 Pin
soulfiremage29-Mar-12 7:37
soulfiremage29-Mar-12 7:37 
GeneralMy vote of 5 Pin
SomPoddar12-Aug-10 9:24
SomPoddar12-Aug-10 9:24 
GeneralInteresting article Pin
Pete O'Hanlon6-May-07 9:18
mvePete O'Hanlon6-May-07 9:18 
GeneralRe: Interesting article Pin
Colin Angus Mackay6-May-07 12:14
Colin Angus Mackay6-May-07 12:14 
GeneralRe: Interesting article Pin
Pete O'Hanlon7-May-07 22:44
mvePete O'Hanlon7-May-07 22:44 
GeneralRe: Interesting article Pin
soulfiremage29-Mar-12 7:41
soulfiremage29-Mar-12 7:41 
QuestionFurther reading? Pin
sadavoya4-Apr-07 6:51
sadavoya4-Apr-07 6:51 
GeneralSome improvements to the article Pin
Crawfis2-Apr-07 12:12
Crawfis2-Apr-07 12:12 
GeneralRe: Some improvements to the article Pin
Colin Angus Mackay2-Apr-07 13:09
Colin Angus Mackay2-Apr-07 13:09 
GeneralCan't say that I agree Pin
Marc Clifton28-Mar-07 16:47
mvaMarc Clifton28-Mar-07 16:47 
GeneralRe: Can't say that I agree Pin
Colin Angus Mackay28-Mar-07 20:45
Colin Angus Mackay28-Mar-07 20:45 
GeneralRe: Can't say that I agree Pin
Marc Clifton29-Mar-07 0:58
mvaMarc Clifton29-Mar-07 0:58 
GeneralRe: Can't say that I agree Pin
Colin Angus Mackay29-Mar-07 4:07
Colin Angus Mackay29-Mar-07 4:07 
GeneralRe: Can't say that I agree Pin
Marc Clifton29-Mar-07 4:25
mvaMarc Clifton29-Mar-07 4:25 
GeneralRe: Can't say that I agree Pin
Colin Angus Mackay29-Mar-07 4:55
Colin Angus Mackay29-Mar-07 4:55 
GeneralRe: Can't say that I agree Pin
wout de zeeuw1-Apr-07 5:54
wout de zeeuw1-Apr-07 5:54 
GeneralRe: Can't say that I agree Pin
smortensen9-Apr-07 16:17
smortensen9-Apr-07 16:17 
GeneralSolid. Pin
Shog928-Mar-07 11:54
sitebuilderShog928-Mar-07 11:54 
GeneralRe: Solid. Pin
Colin Angus Mackay28-Mar-07 12:47
Colin Angus Mackay28-Mar-07 12:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.