That are exactly the problems where TDD tries to and could really help in practice :D.
I just can't see this. The more I read about it, TDD looks like some agile development technique, deliberately promoting the creation of subtly broken software on the basis of YAGNI.
Edit: Sorry, the above came out sounding very dismissive. That wasn't the intent! All I really mean is, I can't see how TDD helps identify edge cases, because from what I'm reading, it sounds like it does the opposite.
I'll describe my own testing methodology by relating to a thing I wrote a year ago. (warning: because this is from memory, the examples are embellished)
I needed a library to facilitate writing wrappers around programs with getopt(1)
style arguments. For instance, to write a wrapper around cp
where -i
(interactive) is the default and -f
(force) can override it. The library would be initialized with all arguments accepted by the wrapped program (and possibly additional args defined by the user), and then it would parse the argument stream into a data structure that can be modified by the user before being serialized back into arguments.
Before writing the code, a variety of examples were clear to me, such as the following. These tests test the result of serializing the parsed arguments without modification, so that they basically just canonicalize the argument stream in the same way getopt(1)
does:
Defined options: -v,--verbose
-o,--output OUTPUT
Input: "prog", "a", "-v", "b"
Output: "prog", "-v", "--", "a", "b"
Input: "prog", "-vv"
Output: "prog", "-v", "-v"
Input: "prog", "--", "-a"
Output: "prog", "--", "-a"
Input: "prog", "-a"
Output: Unknown option 'a'.
Input: "prog", "-o", "-a"
Output: "prog", "-o", "-a", "--"
Input: "prog", "-o-v"
Output: "prog", "-o", "-v", "--"
Had I only coded to the examples that I was capable of dreaming up prior to the implementation, the result would have been, in my opinion, a wonderfully broken piece of software. But this is not what I do.
Instead, during implementation, I play the role of the world's most terrible pessimist. I constantly ask myself how might things go wrong under the current implementation? Or if I changed this statement to do X instead? Or if I made simplification Y to the input? As soon as I think of a counter example, I write it down.
Long story short, what I discovered is that is pretty much impossible to do any form of preprocessing to a getopt
argument stream without full knowledge of the list of valid options. Virtually any attempt to add a lexing stage to a getopt
-style parser ends in total disaster. It's amazing really just how much context-sensitivity can actually exist in what would appear to be such a simple syntax!
Here's some examples that only could have been dreamed up by my inner pessimist during the implementation:
Input: "prog", "-"
Naive output: (crash)
Correct output: "prog", "--", "-"
Input: "prog", "-o-o"
Naive output: Option '-o' requires an argument.
Correct output: "prog", "-o", "-o", "--"
Input: "prog", "-vovv"
Naive output: Option '-o' requires an argument.
Correct output: "prog", "-v", "-o", "vv", "--"
Input: "prog", "-v-n"
Naive output: "prog", "-v", "--", "-n"
Correct output: Unknown option '-'. (or some other error)
Input: "prog", "-o", "--", "-v"
Naive output: (probably crash)
Correct output: "prog", "-o", "--", "-v", "--"
Input: "prog", "-o", "-o", "--", "-v"
Naive output: "prog", "-o", "-o", "--", "-v"
Correct output: "prog", "-o", "-o", "-v", "--"
Input: "prog", "--output=a=a"
Naive output: "prog", "--output=a", "a", "--"
Correct output: "prog", "--output", "a=a", "--"