When You Absolutely Must Use Test-Driven Development

On my team, we try to use Test-Driven Development (TDD) whenever possible, but I confess that we sometimes get either over-confident or lazy. Some of these cases came to light when I surveyed all the defects that we had fixed in a recent release of our software to determine what had caused them.

I’ve been covering many of the causes in this little series on creating fewer defects, but a few defects (3%) could only be chalked up to “There is no way this would have occurred if the developer had used TDD!”

Here is one example. We wanted to ensure our user had entered no more than a given number of digits to the left of the decimal and no more than another given number of digits to the right. The number of digits would vary from case to case, so we decided to generate a regular expression on the fly. There were various complications, such as the fact that even if no digits were allowed to the left of the decimal, it should be okay to enter just a 0, as in 0.99.

Here is the JavaScript the developer submitted for code review.

function getDecimalPattern(digitsToLeftOfDecimal, digitsToRightOfDecimal) {
    var regExp;
    if(digitsToLeftOfDecimal !== undefined || digitsToRightOfDecimal !== undefined) {
        if(digitsToLeftOfDecimal == 0)
            regExp = "^(0?\\.\\d{1," + digitsToRightOfDecimal + "})$|^(0\\.\\d{0," + digitsToRightOfDecimal + "})$|^0$";
        else if(digitsToRightOfDecimal == 0)
            regExp = "^([0-9]|[1-9][0-9]{0," + (digitsToLeftOfDecimal-1) + "})((\\.)|\\.0)?$|^\\.0$";
        else
            regExp = "^([0-9]|[1-9][0-9]{0," + (digitsToLeftOfDecimal-1) + "})((\\.)\\d{0," + digitsToRightOfDecimal + "})?$|^(0?\\.\\d{1," + digitsToRightOfDecimal + "})$";
    }
    else
        regExp = "^-?\\d*$"
    return regExp;
}

Do you know if that’s correct? I certainly don’t. Not at a glance, anyway. After squinting at it for several minutes, though, I did see two problems:

  1. The regular expression generated when the initial if test is true will not allow negative numbers.
  2. Although the if anticipates that the parameters could come in as undefined, the value undefined does not get handled correctly if one value is undefined but the other is not. You could get a result like “^([0-9]|[1-9][0-9]{0,8})((\.)\d{0,undefined})?$|^(0?\.\d{1,undefined})$”.

There were no unit tests for either of these cases. The developer did write unit tests for the new code, but I could tell that he had written them after the fact, which is not how TDD is supposed to work.

How could I tell? If you write tests before the code, while you’re still fresh and enthusiastic about the problem, you will come up with a very different set of tests than if you write them when you are all too ready to move on to something else and you merely attempt to “cover” the code you just wrote.

In this case, a fresh developer would think like this: “I need to test when no parameters are supplied, when only the first one is, when only the second one is, and when both are. In each of those cases, valid numbers I need to test might include negative, zero, and positive. Invalid ones might also include negative, zero, and positive, as well as malformed numbers.”

That simple pause and reflection would have avoided both of the mistakes I found.

The moral of the story is that if you are writing a regular expression, you must use test-driven development. Here are other common cases where a little TDD goes a long way.

  • Promise-based code (the subject of the last post).
  • Error-handling. (This is especially important to unit-test because the QA team might not encounter the error in their tests.)
  • Any function with high cyclomatic complexity (basically many possible branches).

Leave a Reply

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