Test- and behavior-driven development is becoming the de-facto method of development in the ruby community, at least among the influential and hard-blogging dev shops like Thoughtbot, Pathfinder, and Pivotal. If you follow those blogs, as I do, you’ve seen some crazy awesome testing stuff like testing capistrano with cucumber, and 50 Cent showing the money you could be saving with unit testing. Unfortunately, one problem that pops up frequently with the test first paradigm and isn’t discussed widely enough is “what do you do when you don’t know enough about the code you’re working on to write a test first?” The standard advice given by TDD advocates is to code one or more prototypes, then throw them away and use what you learned to rewrite the "real" code, testing first this time.
The Problem with Prototypes
Now, I’ve become a TDD convert ever since I forced myself to try it a few years ago to see how it would turn out (awesome, of course). Sure, I went through a sort-of larval phase, during which I would sometimes write one or two methods ahead and then go back and add tests afterward, but now I’m almost 100% test first. I’ve always had an issue with prototyping, though. One problem is that it’s hard for me to throw away code I spent all that time working on (even if “all that time” was only five minutes). I’ll convince myself to do the right thing and throw it away, but then I’ll be kinda bummed and my tests will be more of a verification of the prototype code than a true rewriting. The process usually ends up feeling like adding tests to an existing codebase (do. not. want.) than the fun experience I have when I’m in the TDD “zone.” Another option is to test first anyway, making uninformed guesses about what the API should be until, after lots of staggering around and a couple cycles of scorched earth refactoring, I arrive at something acceptable. Is there a better way?
Prototyping the API
I recently bumped up against this particular wall working on Voracious, a real-time log analysis application I’m writing (private github repo for now). I have a few classes to scan log files and do the analysis, and I’ve decided to use an assembly line style of design with AMQP gluing everything together. What I want to work on next is the configuration class, which will allow the user to configure the application in ruby code. I know this class needs to set up the file scanners and analyzer objects, and depending on how well it fits, I might have it run the main loop. But I have no idea what the code should look like, so I’m staring at a blank configuration_spec.rb. What to do? Prototype, of course. But this time I didn’t prototype any code in the class. Instead, I prototyped the API. I put something like this at the top of configuration_spec.rb:
Configuration.new do |c|
# First attempt
# This should create a scanner for the file and setup the routing keys in AMQP
c.file("/path/to/access.log", :type => :apache, :analysis => [:anomaly, :ip], :app => "test_web_app.com")
# Next try
hosts("app1", "app2", "app3") do |app_servers|
# other options use defaults
app_servers.file("/path/to/access.log", :type => :apache)
end
# Maybe define groups?
group :app_servers => ["app1", "app2", "app3"]
# Use yaml so new group members can be added from web UI?
group :app_servers, :file => "/some/yaml/file.yml"
# look in the default directory for a file named app_servers.yml
group :app_servers, :file => "app_servers"
# Setup scanners
app("test_web_app") do |app|
app.analyzer :anomaly_detector, :property => :relative_url, :parallel => 4
end
end
The first thing to know about this code is that none of it runs. The point isn’t to produce running code, but to produce failing tests. Now that I’ve tried a few different possibilities for the API, I can pick the bits that I like as the basis of my failing tests:
describe Configuration do
it "should create a scanner object and analyzer routing keys for a file" do
config = Configuration.new do |c|
c.file("/path/to/access.log", :type => :apache, :analysis => [:anomaly, :ip], :app => "test_web_app.com")
end
config.should have(1).files
config.files.first.path.should == "/path/to/access.log"
# etc. ...
end
end
From here, I can continue to take out bits from my prototype, and turn them into failing tests. I’ll probably need to refactor a bit more than usual as I build out the code because I’m still not 100% sure how the code should be structured, but I’ve got a solid, test-driven start where I was totally stumped before.
Conclusion
I don’t expect that this will completely eliminate the need for prototyping code-first. If you know what the API should be but you’re uncertain about how the code should actually work, you might have to resort to some variation of inside-out prototyping. Also, having user stories and/or a higher level testing framework such as cucumber may make the API more obvious, so you won’t need to use a technique like this very often. Despite these reservations, I’ve found that prototyping code that uses the code I’m trying to write gives me a great way to try APIs before I write any tests against a crappy API, gives me a jump start when I’m staring down a blank test, and helps keep me in that mythical TDD “zone.”