I am a bit late to the game here, but I wanted to mention that I am quite a fan of behavior-driven development. When I was first introduced to behavior-driven development via RSpec, I was deeply unimpressed: all the tutorials and example code I saw looked like verbose re-writes of standard unit tests. Worse, it injected weird magic onto testing which obscured what was actually going on from a developer standpoint, which made it harder to debug why a particular failure occurred.
Something clicked this morning, though. On Twitter, someone asked What’s the argument against testing private methods again?, and I responded with You should only test private methods insofar as they impact user API. Test contracts: don’t just rewrite your code in asserts. And I suddenly realized the point of BDD.
The problem that BDD solves is unit testing code like this:
def doFoo() { myFoo() fooListener.onFoo(this) }
The traditional way to approach this is to create a unit test to test doFoo, and in there validate that myFoo() was called by asserting its side effects, and then mock up a fooListener that checks for onFoo to be called with this as an argument. In short, the traditional way is to rewrite this code in assert statements.
But that’s not the worst part. The worst part is that the resulting unit test is simply floating in the air, without context. If it fails, it is completely unclear whether or not that failure is expected and acceptable (cruft to be removed) or if it is a critical piece of functionality that can’t be touched. The “why” behind the code is completely lost with parrot-style unit testing.
Behavior driven development (hereafter BDD) solves that problem. I have been working with BDD via EasyB (from whom I am stealing the scenario below). The thing that is distinct about BDD is that instead of just having a hunk of code that throws an exception if something changes, BDD drives the developer to think in terms of context and contract.
A unit test for a stack class might look something like this:
void testNull() { shouldFail(RuntimeException) { stack.push(null) } assertTrue stack.empty }
In this case, a failure is simply reported as an AssertionError and a line number. The developer then has to struggle to figure out what exactly was intended on being tested here. In this case, what the code is testing is only slightly obtuse — in even slightly more complex cases (like the doFoo method above), it can be downright opaque.
It’s much more obvious what you’re talking about when you do something like this:
scenario "null is pushed onto empty stack", { given "an empty stack",{ stack = new Stack() } when "null is pushed", { pushnull = { stack.push(null) } } then "an exception should be thrown", { ensureThrows(RuntimeException){ pushnull() } } and "then the stack should still be empty", { stack.empty.shouldBe true } }
It’s certainly more verbose, but the resulting test provides an actual contract for the stack class: something that can be used as an actual API to code against. This has always been the unfulfilled promise of unit tests: they actually provide a way to understand a class from the outside. When you run a string of these scenarios, you end up with an output file that looks like this:
33 specifications (including 2 pending) executed successfully
Story: empty stack
scenario null is pushed onto empty stack
given an empty stack
when null is pushed
then an exception should be thrown
then the stack should still be empty
scenario pop is called on empty stack
given an empty stack
when pop is called
then an exception should be thrown
then the stack should still be empty
Story: single value stack
scenario pop is called on stack with one value
given an empty stack with one pushed value
when pop is called
then that object should be returned
then the stack should be empty
scenario stack with one value is not empty
given an empty stack with one pushed value
then the stack should not be empty
scenario peek is called
given a stack containing an item
when peek is called
then it should provide the value of the most recent pushed value
then the stack should not be empty
then calling pop should also return the peeked value which is \
the same as the original pushed value
then the stack should be empty
then an example pending [PENDING]
[...]
Someone new to the code — even one who doesn’t know the language the code is written in — can easily read through that write-up and get a very solid sense of the behavior of the class, including functionality that is on the docket but not yet implemented. That’s pretty awesome.
Related posts:
Pingback: Enfranchised Mind » How Should I Burn My Free Time?