“Test-driven development” is one of those things that causes hives among some programmers, who immediately stand up and plant the claim that it’s worthless, peurile, deceptive, and generates awful code… never mind that others manage to use it effectively.
I have been playing with a 2D cellular automaton, largely inspired by Stephen Wolfram’s “A New Kind of Science.” I have a version in Java, and a simpler implementation in Python. TDD made the Java version work properly, and a lack of TDD left a hole in the Python version.
This post is not about the automaton itself. I’ll write that up later. (If you’re interested, you can see the source.)
What is TDD?
Test-driven development is, loosely defined, a practice in which tests are written before anything else, without regard to correctness.
For example, if I want to write a program to generate “Hello, world,” I would write a test that validated that “Hello, world” was generated – before I had anything that might create the output. My tests would fail; they wouldn’t even compile until I had some sort of implementation written.
However, by writing a test before anything else:
- I more or less force myself into having some sort of specification that says what “correctness” means for my program (it is “correct” when it generates “Hello, world”)
- I also force myself into writing something that has a reasonable interface (because I’m writing how I think it would be called, before writing the guts of the implementation)
By writing the tests first, I’ve effectively given myself a criterion for completeness. When my tests pass, I know I’ve “finished,” because my tests define a specification.
There’s nothing wrong with the specification being incomplete, of course; it may so happen that later, I want to greet someone specific. By having tests in place, though, not only do I have a record of the specification, but I also have a way that I can add to the specification in such a way that I know I’m not breaking code – I would simply add more tests that corresponded with the changing specification, and I will know if my changes break other code.
How did TDD work out for my Automaton?
Here’s the thing: I wrote the Java implementation using test-driven development practices (TDD), and the automaton is kinda neat; it generates some fascinating patterns even without entropy or a variable starting cell structure. An example, of pattern 171, using a color rendering mechanism:
The Python version was written because a friend of mine wanted to consider using it for a class he’s teaching. The Python version is very much simpler than the Java version, because it doesn’t do as much (it can’t output to multiple formats, for example).
It was not written with testing in mind. Why would it be? I had written the Java version from tests first; I was only writing a simple port to Python.
It was also wrong. A 2D automaton can “grow” to the right and to the left, depending on the pattern it’s given; the Python version could only grow on the right, because I had an off-by-one error in a core routine.
TDD would have caught that early (and it did catch problems like that, in the Java version).
TDD also provided me the opportunity to fix the names of structures (renaming
Generation, for example) because the tests made it obvious that the names were inaccurate.
Could I have done it without TDD? Of course. TDD isn’t the only way to write programs well. It’s not the only tool used to work out good names, or good processes, or even to validate that programs work properly – the Python version of the automaton was fixed without TDD, for example.
If you’re wondering why I didn’t use TDD for the Python version, it’s because I’m too much of a newbie with Python to know how, yet – and as I’m not really a Python programmer, there’s not a lot of need. However, seeing the differences in the development process between my Python implementation and the Java implementation, I might look into TDD with Python anyway.