Friday, July 20, 2012

Specify What, Not How



It was January 11, 2012 and I was attending Jim Weirich’s session on Vital Testing at CodeMash. Now I’ve seen his talks numerous times and continue to attend his talks any chance I have. Why? Because Jim is just that awesome! There is always something Jim says that triggers a challenge within myself that leads to improvement in how I approach crafting software. That day it was, “Specify what, not how

I already knew that specs should not describe implementation and I prided myself in being able to abstract the implementation details away from my specs. But as Jim elaborated further on specifying behavior and what was sufficient, I realized that I could not entirely rewrite my apps from just the specs. Why? Because even though I had abstracted away implementation details, my specs still contained some implementation details.

Take this snippet for example. What’s wrong with it?


Most Ruby developers would say nothing. And I used to too. Now look again at the very beginning. What is it that is being specified? The class name. The class name is being directly stated in our specification identifying how we are implementing our behavior. This is typically an accepted approach when using RSpec, but if our goal is to achieve the writing of specifications and remove implementation details from our code, then this is not ok.

“Ok then, how should it look?”, you ask. Let’s take a step back. In our example we are trying to describe converting roman numerals to integers, so let’s write that.



Ok, easy enough and it reads nicely and clearly states our intentions. “But how do you write the actual test?” Great question. If you could write it anyway you wanted, how would it look? Using our first example I propose “1 should be converted to I”. That’s great, but it will not execute without some heavy lifting. Not to fear! With a little tweaking we could write it instead as:

1.should be_converted_to "I"

Awesome (I <3 Ruby)! That is something we can work with. And the magic sauce that brings it all together ... custom matchers! Custom matchers easily allow us to be more expressive in how we write our specifications and provide nice wrappers for our implementation details.

Here is an example of what a custom matcher would look like based on how we just wrote our test.



All of our implementation details have now been abstracted away from the specifications and wrapped up in this cool little matcher. Now when we decide to change our implementation details we can leave our specifications alone and focus solely on our matcher. Expanded out a bit further our specifications could look something like this:



I don’t know about you, but those specifications are much easier to read AND to maintain. And yes, this does translate to your project. We've been taking this approach since January and have seen big gains in reducing any brittleness in our tests, but also we’ve seen the quality of our code increase significantly! 

The custom matchers have also helped identify when an object is doing too much. So when setup starts to creep into our matchers we quickly reevaluate our approach and refactor to remove the setup from the matchers, resulting in more independent objects. (Gotta love SRP!)

I hope this helps you in your quest to write better specifications. Special thanks to Jonathan Knapp for pairing with me that day as we explored this approach together in our quest to best roman numerals!

3 comments:

  1. I worry about duplicating equality comparisons, so I'd probably do this instead, in each spec: convert(1).should == 'I'; convert(2).should == 'II', and so on. Then I define #convert inside my context to invoke Calculator.convert(actual). Not quite as cute, but avoids reimplementing parts of "should equal to", as your example has done.

    Even so, I like the thinking behind the example and encourage everyone to experiment with it.

    ReplyDelete
  2. JB, I don't follow. The matcher is still only performing one comparison per spec, not two. Is there some magic in the custom matcher implementation that performs an additional check I am unaware of? The matcher approach also produces more meaningful test output, which you don't get with convert(1).should == 'I'.

    I really wanted to do something along the lines of 2.converts_to 'II', however I'm not about to monkey patch integer for the sake of a test.

    And yes, very cute ;)

    ReplyDelete
  3. I appreciate this to a degree, but it feels like you're just pushing things into an abstraction layer that's harder to find.

    There is something worrisome to me about 'just push to another layer' approaches. 1000 layers to manage is harder than 1000 lines of code in a single file.

    Just my $0.02

    ReplyDelete