Why I'm Still Guido's Subject
One of the main reasons I think people aren't willing to do test-driven development is because they don't often understand a problem well enough before they set out to solve it. This is especially true for me. I tend to feel my way around everything, building a very trial-and-error sense for what's going on before I can say that I'm developing the solution.
Enter Python. Python is a perfect tool for doing things you don't understand – which is why people dig it so much. Unlike other languages that make you have a hard, mathematical understanding of what you're doing, Python lets you get a more heuristic sense for the computer's relationship to your understanding of the problem. I used to love Python for this reason but if you ask Python to do some real industrial strength computing (such as read a bytestream) its limitations start to glare.
But Python's greatest strength is that it can help you spam-out code that you don't need to fall in love with before it works. The cruftiest solution man can fathom is usually moments and rarely more than a couple hours away. This is often confused for an increased productivity among pythonistas.
Whatever warts it may have, Python stil serves terribly well as a means of coming to understand problems (if you want to) while groping around for a solution.
As a result, I tend to use Python quite a lot. It serves terribly well as a prototyping system where I can build a make-shift solution to what I think is a problem, observe its behaviors, and determine if that actually is both the problem and the solution I'm after.
When I've done this, I can distill my problem into a series of byte streams after the fashion of the Unix mind set; splitting my solution into separate Python scripts that each perform a necessary part of the solution. The result is a command line that may look something like this:
$ cat problem.txt | ./stage1.py | ./stage2.py | ./stage3.py > out.txt
When I believe the output is good, I turn it into an automated test like so:
$ cat problem.txt | ./stage1.py | ./stage2.py | ./stage3.py > out.txt $ cp out.txt good.txt $ cat problem.txt | ./stage1.py | ./stage2.py | ./stage3.py > out.txt && diff good.txt out.txt
Each individual unit can also be tested.
$ cat problem.txt | ./stage1.py > out1.txt $ cp out1.txt good1.txt $ cat problem.txt | ./stage1.py > out1.txt && diff good1.txt out1.txt $ cat good1.txt | ./stage2.py > out2.txt $ cp out2.txt good2.txt $ cat good1.txt | ./stage2.py > out2.txt && diff good2.txt out2.txt $ # etc.
I can put the necessary commands into a shell script that will check the return code of each call to diff and throw big ugly "ISHY, YOU MISERABLE FAILURE" messages when the output received doesn't match the good copy of the output.
After I'm confident that this will solve my problems for real, I begin to write actual code – likely in C. Each python script that represents a stage in the solution is replaced with a C program that does the same thing. I'll usually work backwards unless I have a strong engineering reason to go another way.
As I develop, each component can be massaged into the solution and tested gracefully. Testing the C replacement for stage 3 only requires us to change one thing in the pipeline and the single component is tested within the context of the bigger picture.
$ cat problem.txt | ./stage1.py | ./stage2.py | ./stage3 > out.txt
The make file will, after compiling each component, subject it to tests and, at the end of the ``all'' target, test the entire system together in the following fashion:
$ cat Makefile CC=clang all: stage1 stage2 stage3 cat problem.txt | ./stage1 | ./stage2 | ./stage3 > out.txt && diff good.txt out.txt stage1: stage1.c stage1.h $(CC) stage1.c -o stage1 cat problem.txt | ./stage1 > out1.txt && diff good1.txt out1.txt stage2: stage2.c stage2.h $(CC) stage2.c -o stage2 cat good1.txt | ./stage2 > out2.txt && diff good2.txt out2.txt stage3: stage3.c stage3.h $(CC) stage3.c -o stage3 cat good2.txt | ./stage3 > out3.txt && diff good3.txt out3.txt
Naturally, in real-world projects, I end up with more forms of input than just a single dump of problem text – which leads to many more lines of test in the makefile. I usually end up using code to generate these lines so I have confidence that I'm testing every permutation of the imaginable inputs.
The whole point to this brain-dump is that I strive (though not always with resounding success) to do TDD and Python is an integral part of arriving at a sound test for me. It's come to be the most valuable thing Python has to offer me as my programming matures.