Recently, I was working on a refactor, on a script to generate DNS entries for company switches. The way DNS currently works is, for every IP on the server, we make a forward and reverse entry for that IP in dns. So, if you do a traceroute, the switch name will show up. But I was asked to instead change DNS, so that each IP address would report the name of the port. So, for a comparison, here's a simplified example of the old DNS entries for a switch
sw1 IN A 10.0.0.1 #switch1 management port
sw1 IN A 10.0.0.2 #switch1 Gi0/1
sw1 IN A 10.0.0.3 #switch2 Gi1/2
doing a traceroute would report the same 'switch' name, no matter which port the routing went through.
now, it reads something like this:
sw1 IN A 10.0.0.1
sw1-gi0-1 IN A 10.0.0.2
sw1-gi0-2 IN A 10.0.0.3
The corresponding reverse lookups are now more informative, because the network engineer can see, right in the traceroute, which port is being traversed. It saves them the step of logging into a switch, to match up IP's with ports by hand, and can save them a lot of time troubleshooting.
I only recently adopted TDD, but today, I decided, without really thinking about it, to just crank out the changes I wanted to make.
So here's what I did:
- I added the functionality
- I edited my tests
- I ran them, and they passed
It was awesome, and a rare experience, to have tests pass on the first try. But the very fact that I didn't see a 'failure' before I made the code changes immediately worried me. These changes were actually a pretty big deal; changing DNS entries across an enterprise is a big deal. So I edited the test to fail, reran it, saw it fail, then edited it back to pass, reran it, and saw it pass. And I was back to feeling confident.
So, what was making me anxious at that point? Just the fact that I'd violated the dogma that TDD is the best way to develop? Would I have actually gained anything, if I'd written the test before writing the code, or was I just being a paranoid? I thought about it a bit, and came up with a few reasons for why I would have been happier with my code, if I'd:
- Written the tests first
- Ran them, just to see them fail
- Edited my code
- run the tests to see them pass
I think it's the first time I actually understood the point of that first, somewhat redundant-looking step. Here's my reasoning:
- If I add a test, then run it, and it passes automatically, then I get the chance to reevaluate why. If TDD does nothing more than save me from spending an hour troubleshooting a problem, only to find out that the reason I can't fix it is that I've been editing the wrong file, or running the wrong script, that's a huge gain. Been there multiple times, done that multiple times, didn't like it.
- If I start by writing tests, I have to open my existing tests, and at least read enough of them to find a place to add my new tests. This refreshes my memory on how the code works, which reduces the chances of accidentally duplicating functionality I already have.
- It slows me down, and gives me a chance think about what I'm doing. This means I don't have to rewrite my method several times, just to fix my initial, knee-jerk solution.
So, given this, why don't we all religiously adopt TDD development? Why do people like David Heinemeier Hannson deliver keynote speeches about why TDD isn't the end-all, be-all of testing? Why does he liken TDD to fad diets? I'm still pretty new to coding, but I want to take a whack at this. First, I think that David wasn't actually decrying testing itself, at least not as much as it seems he was. During his talk, he spent a lot of time on caveats. His point, which I think was universally misunderstood, was that we shouldn't follow a TDD philosophy as dogma, and expect that it will save our butts in every case. He specifically said that there IS a place for TDD, and for unit testing. But, especially in Rails, testing from the front-end, using tools like Capybara, can get you much better results, by testing, empirically, that the application works from the end-user perspective. Unit testing each method, in four different ways, is very different from running an end-to-end transaction, from the point of view of the browser. For a webapp, TDD is sometimes impossible, or at least, unproductive. How do you create a test that browses to your website, adds items to a cart, and checks out, before you write any code? It's not useful, at least at this macro level. That, I think, was David's point.
This takes me to my final observation; the biggest barrier for me, when building code in a TDD way, is the sheer effort it takes to write those tests. I have to pick a testing framework, learn that framework, learn to write code that's easy to test, and constantly switch between editing tests and editing actual code. For me, the tradeoff was a huge loss of productivity.
But I found a way to fix that.
This is the good part, by the way. I essentially wrote this whole post, in order to write the following paragraph. I write it in first person, describing what I did, because I'm a newbie, and don't feel I have the authority to tell people that they should do these things. But you probably should.
So, I made a macro in my text editor that generates the standard testing template for my test suite, with only a few button presses. Also, I learned enough about Rake to run tests from there with only one command, and created a couple of aliases at the command line, so that I could run tests with less typing. I learned how to create tabbed displays in my text editor, so that I could view both my test file, and the code being tested, without switching back and forth. I learned how to split my screen, so that I could have a command line, and my text editor instantly available, and always in the same spots. So, now, I can change focus from tests, to code, to command line, with only a few key presses. It's so easy, I don't even have to think about it. And I created a bookmark in my browser, so that I could easily reference manuals related to my test suite, and to my current work. And, over time, I've accidentally memorized about three syntactical bits of my test suite. That's all it took to 'master' testing. Three things.
Keep in mind, I didn't do any of this with the 'goal' of being able to do faster test-driven development. It was a natural progression, guided by my desire to spend less time fighting with my text editor, or the Unix command line. Mastering the tools we spend most of our time in carries self-evident, intrinsic rewards.
By accidentally eliminating these barriers, I've come to a place where the only tradeoff to doing TDD is the cost of actually thinking through tests, looking at what the class already does, considering what my new method should return, and coming up with a method name, before I actually create that method. And, thinking about code from a testing-first perspective forces me to follow the simplest path, which leads to cleaner, more flexible code. These tradeoffs, for me, carry their own benefits.