A peek into Test Driven Design
As the software that we write grows in complexity, there’s been an increasing trend amongst practitioners to guarantee or prove that their programs operate as desired. No serious programmer will deny that this ability is one of the most important ones in running complex software systems. Without such guarantees, it becomes impossible to state anything meaningful about the system and refactoring the system or optimising the performance of it becomes fraught with risk.
There are several approaches to getting these guarantees from the system and unit testing is a component of most of these approaches that I am aware of. Test Driven Development is a well known term and especially for dynamic languages like Ruby, it is a phenomenal methodology for building systems that work as advertised. However, the correctness of the program is just one of the good things that happens when testing drives development. Testing also has the ability to drive the design of your software. I recently had one such a-ha moment related to test driven design that I’d like to share with you.
So I was looking at some test code that I wrote some time ago. Let’s look at the code in all its undecipherable complexity. This is some test code to test a state machine implemented with the wonderful workflow
gem
wow! horrible, right? I mean, you can kind of tell whats going on, but there’s way too much text over there. Another smell - comments in your test. @ponappa and @aninda42 did an awesome session on code smells in tests at RubyConfIndia 2012, and they are 100% right - your test code should not need comments. And yes, multiple assertions in one test case. Another code smell. So, what’s the fix?
Here’s where unit testing can really help to drive the design of your code. So we’re looking at these horrible tests and thinkng - why am I testing the workflow gem? Given that the gem itself is extensively unit tested, I should not really have to worry whether a given transaction succeeds or not. Instead, I should concentrate on checking whether my workflow is correctly defined, rather than whether the definition of the workflow leads to correct actions. The latter part is the responsibility of unit tests on the workflow gem.
So, with this is mind, we start to rewrite our tests without the code smell. Now, they look like this.
Oh, much better. Specs are totally readable. It’s apparent at a glance what is going on there and we’re not busy messing with testing Workflow functionality, we’re only testing our workflow definition. This is akin to saying it { should have_property(:foo)}
as we do for DataMapper models.
Now here’s the rub. The workflow gem is not able to answer the question has_events?
properly. Sometimes a state will have a particular event associated with it, but the transition is not possible due to some guards around the transition. We need to update the workflow
gem and add a method called has_events?
, which correctly filters out events which exist but which are not possible.
This is where the design is being driven by the tests. We update our workflow DSL to accept an :if
clause, write some tests to make sure the workflow is respecting the clause and voila, we’ve contributed to open source, made our tests much cleaner and more readable and ended up with a nicer workflow DSL in the bargain.
You can find my changes in the workflow gem here: https://github.com/svs/workflow/tree/update
This is just a small example of how you can use test code smells to drive better software design.