Anti-specs: Using RSpec to document the bad in addition to the good
Thursday, December 13th, 2007Sometimes we purposely write code that has bugs that we don’t intend to fix.
I know that sounds terrible, but consider the following example. We were recently writing a class to parse a known set of data that didn’t include any whitespace between tokens. Our one-time class worked great for this particular data set, and we didn’t anticipate having to use it on another data set. However, if you were to pass it similar data that did have whitespace between tokens, then you wouldn’t get the desired output.
We could have spent a bunch of time expanding the class to handle whitespace properly, but we quickly realized that this would have doubled the development time for the class, all for adding a feature that we weren’t going to use.
Instead, we added some anti-specs for the class that documented the undesirable behavior:
describe "Unfortunately, #quote_nodes" do
it "will preserve whitespace around each node" do
quote_nodes(" snap , crackle ,( pop )").should == "' snap ',' crackle ',(' pop ')"
end
it "will quote whitespace as a node if it appears between delimiters" do
quote_nodes(" , , ( ) ").should == "' ',' ',' '(' ')' '"
end
end
Note how instead of the usual RSpec phrasing, e.g. “#quote_nodes … should put single quotes around each node”, we start the context with “Unfortunately” to immediately tell the reader that this is undesirable behavior, and we use the word “will” instead of “should” to tell you that this isn’t what we would want in an ideal world, but it’s what we’re getting anyway. You wind up with spec docs that clearly document the good and the bad:
#quote_nodes:
- should put single quotes around each node
- should quote nested nodes too
- should only treat parentheses and commas as delimiters
- should preserve whitespace within nodes
Unfortunately, #quote_nodes:
- will preserve whitespace around each node
- will quote whitespace as a node if it's between delimiters
Down the road, we may wind up re-using this code in another application. If we need to fix the bad behavior, we can simply modify the specs to document the new desired behavior, and then add the additional code to properly handle whitespace and make the tests pass.
I didn’t see anyone else writing about this use of RSpec, but we’ve found it to be useful for these somewhat rare cases.
There’s no reason why you couldn’t do this with Test::Unit and have “anti-tests”, but I particuarly like the phrasing that you get to use with a BDD framework such as RSpec or shoulda.
