Telerik blogs

If you’ve never done Test Driven Development or aren’t even sure what this "crazy TDD stuff” is all about than this is the series for you. Over the next 30 days this series of posts take you from “I can spell TDD” to being able to consider yourself a “functional” TDD developer. Of course TDD is a very deep topic and truly mastering it will take quite a bit of time, but the rewards are well worth it. Along the way I’ll be showing you how tools like JustCode and JustMock can help you in your practice of TDD.

I’ve previously discussed a bit of the TDD workflow; start with a requirement, derive a test from the requirement, write just enough code to make that test pass, repeat. This is sometimes referred to as “Red, Green, Refactor” which I’ll be coming back to several times over the course of this series. In this post I’ll show you how this approach can be extended to dealing with software defects.

Previous Posts in this Series Day Seven – Software Factories and DI Frameworks

 

Bad Defects Happen to Good Software

No matter how good you are at writing code, no matter how closely you follow the requirements and not matter how much you test your software, defects are a fact of life. While TDD will help you reduce the number of defects, I’m afraid that there is just no way of completely avoiding them. Maybe it’s a requirement that the business didn’t think through or missed. Maybe it was a misunderstanding of the requirements. Of perhaps it was a technical aspect of your design that you simply missed. Regardless, defects are a fact of life if you develop software. The good news is that if you’re practicing TDD your workflow for fixing defects not only is easy, but when applied properly can help assure that a fixed defect stays fixed.

To demonstrate this, let’s go back and look at the example from days three and four. To refresh your memory, I was tasked with writing a method that would count occurrences of a single character in a string. To achieve this I created the following tests:

 1: using System;
 2: using System.Linq;
 3: using NUnit.Framework;
 4:  
 5: namespace ThirtyDaysOfTDD.UnitTests
 6: {
 7:     [TestFixture]
 8:  public class StringUtilsTest
 9:     {
 10:         [Test]
 11:  public void ShouldBeAbleToCountNumberOfLettersInSimpleSentence()
 12:         {
 13:             var sentenceToScan = "TDD is awesome!";
 14:             var characterToScanFor = "e";
 15:             var expectedResult = 2;
 16:             var stringUtils = new StringUtils();
 17:  
 18:  int result = stringUtils.FindNumberOfOccurences(sentenceToScan, characterToScanFor);
 19:  
 20:             Assert.AreEqual(expectedResult, result);
 21:         }
 22:  
 23:         [Test]
 24:  public void ShouldBeAbleToCountNumberOfLettersInAComplexSentence()
 25:         {
 26:             var sentenceToScan = "Once is unique, twice is a coincidence, three times is a pattern.";
 27:             var characterToScanFor = "n";
 28:             var expectedResult = 5;
 29:             var stringUtils = new StringUtils();
 30:  
 31:  int result = stringUtils.FindNumberOfOccurences(sentenceToScan, characterToScanFor);
 32:  
 33:             Assert.AreEqual(expectedResult, result);
 34:         }
 35:     }
 36: }

(get sample code)

The algorithm I used to make these test pass can be seen here:

 1: using System;
 2:  
 3: namespace ThirtyDaysOfTDD.UnitTests
 4: {
 5:  public class StringUtils
 6:     {
 7:  public int FindNumberOfOccurences(string sentenceToScan, string characterToScanFor)
 8:         {
 9:             var stringToCheckAsCharacterArray = sentenceToScan.ToCharArray();
 10:             var characterToCheckFor = Char.Parse(characterToScanFor);
 11:  
 12:             var numberOfOccurenes = 0;
 13:  
 14:  for (var charIdx = 0; charIdx < stringToCheckAsCharacterArray.GetUpperBound(0); charIdx++)
 15:             {
 16:  if (stringToCheckAsCharacterArray[charIdx] == characterToCheckFor)
 17:                 {
 18:                     numberOfOccurenes++;
 19:                 }
 20:             }
 21:  
 22:  return numberOfOccurenes;
 23:         }
 24:     }
 25: }

(get sample code)

After this code was deployed for some time a defect was reported;

A user was able to pass in a two character string for the second argument to FindNumberOfOccurences and an exception of type FormatException was no thrown. The expected behavior is for the method to throw an exception of type ArgumentException.

 

When you think about it, defects are simply another type of requirement. A “traditional” requirement describes how your software should work whereas a defect describes how your software should have worked. When you use TDD and build your software around tests that have been built around requirements you should be addressing all of those requirements in your code. If this is true then defects are nothing more than new requirements that you didn’t know existed or refinements of existing requirements. These requirements can be “functional” requirements (the webpage doesn’t work in IE 6) or business domain related, as the one in our example is.

Regardless of what type of new requirement we have, the workflow is the same, I first write a test. In this case I want to write a test that demonstrates that the current behavior as described by the defect is in fact an error, thus the test should fail:

 1:         [Test]
 2:         [ExpectedException(typeof(ArgumentException))]
 3:  public void ShouldGetAnArgumentExceptionWhenCharacterToScanForIsLargerThanOneCharacter()
 4:         {
 5:             var sentenceToScan = "This test should throw an exception";
 6:             var characterToScanFor = "xx";
 7:             var stringUtils = new StringUtils();
 8:  
 9:             stringUtils.FindNumberOfOccurences(sentenceToScan, characterToScanFor);
 10:         }

(get sample code)

This looks similar to our previous test, but there are a couple of differences. For one, I am not specifying an expected result from the call to FindNumberOfOccurences, nor am I capturing the result of that call. As I don’t have either of these values, I’m not making a call to Assert.AreEqual either. That’s because my expectation for this test is for FindNumberOfOccurences to throw an ArgumentException. If you look on line two you will see that I’ve added an additional attribute to this test. The ExcpectedException attribute is part of the Nunit framework and tells the test runner that this test should throw an exception of type ArgumentException. If this exception is thrown, the test passes. Otherwise if fails.

My expectation is for this test to fail the first time I run it and I am not disappointed (Figure 1):

image

Figure 1 – The new test fails

If you remember our TDD workflow you’ll know that the next step is to try the simplest thing that may work. For this example I’m going to change my implementation of FindNumberOfOccurences to throw an exception right away:

 1:  public int FindNumberOfOccurences(string sentenceToScan, string characterToScanFor)
 2:         {
 3:  throw new ArgumentException();
 4:  
 5:             var stringToCheckAsCharacterArray = sentenceToScan.ToCharArray();
 6:             var characterToCheckFor = Char.Parse(characterToScanFor);
 7:  
 8:             var numberOfOccurenes = 0;
 9:  
 10:  for (var charIdx = 0; charIdx < stringToCheckAsCharacterArray.GetUpperBound(0); charIdx++)
 11:             {
 12:  if (stringToCheckAsCharacterArray[charIdx] == characterToCheckFor)
 13:                 {
 14:                     numberOfOccurenes++;
 15:                 }
 16:             }
 17:  
 18:  return numberOfOccurenes;
 19:         }

(get sample code)

You’ll see on line three I am simply throwing a new instance of ArgumentException. If I run my test I can see that it’s now passing (Figure 2):

image

Figure 2 – Our test now passes

The new test passes. Before we can say we’re done though we need to make sure we haven’t broken anything. We can do this by running our entire test suite (Figure 3)

image

Figure 3 – We still have problems

While we have fixed the defect, we have broken something else. This is a great demonstration of the value of TDD and unit testing in general. Without my existing unit tests I would have no easy way to verify that my existing functionality was not effected by my change. We’ll touch on this more in a future post in this series where I discuss refactoring.

In the meantime I need to make all my tests pass. I can do this by making a small alteration to the implementation of the FindNumberOfOccurences method:

 1:  public int FindNumberOfOccurences(string sentenceToScan, string characterToScanFor)
 2:         {
 3:  try
 4:             {
 5:                 var stringToCheckAsCharacterArray = sentenceToScan.ToCharArray();
 6:                 var characterToCheckFor = Char.Parse(characterToScanFor);
 7:  
 8:                 var numberOfOccurenes = 0;
 9:  
 10:  for (var charIdx = 0; charIdx < stringToCheckAsCharacterArray.GetUpperBound(0); charIdx++)
 11:                 {
 12:  if (stringToCheckAsCharacterArray[charIdx] == characterToCheckFor)
 13:                     {
 14:                         numberOfOccurenes++;
 15:                     }
 16:                 }
 17:  
 18:  return numberOfOccurenes;
 19:             }
 20:  catch
 21:             {
 22:  throw new ArgumentException();
 23:             }
 24:         }

(get sample code)

In this case I’ve elected to fix the code by wrapping the entire method in a try/catch block. If an exception is thrown I swallow it and throw a new ArgumentException. I know this is not a good way to do this, but don’t worry. For now it satisfies our test and we’ll be revisiting this code soon in a post about refactoring.

By running the tests again I can see that now not only has my defect been fixed, but my previous functionality has also been preserved (Figure 4):

image

Figure 4 – All of the tests pass

Summary

As practitioners of TDD we should think of defects as simply more requirements. Therefore we use the same workflow by writing a test first, then writing code to make our new test, and all the other previously existing tests pass. Not only does this demonstrate that we’ve addressed the defect, we can insure that as time goes on this defect does not reoccur.

In the next post we’ll start talking about refactoring.

Continue the TDD journey:

JustCode download banner image

JustMock banner


About the Author

James Bender

is a Developer and has been involved in software development and architecture for almost 20 years. He has built everything from small, single-user applications to Enterprise-scale, multi-user systems. His specialties are .NET development and architecture, TDD, Web Development, cloud computing, and agile development methodologies. James is a Microsoft MVP and the author of two books; "Professional Test Driven Development with C#" which was released in May of 2011 and "Windows 8 Apps with HTML5 and JavaScript" which will be available soon. James has a blog at JamesCBender.com and his Twitter ID is @JamesBender. Google Profile

Comments

Comments are disabled in preview mode.