Behavior-Driven Development Using Ruby (Part 2)
Pages: 1, 2, 3, 4
By running our specs, we get a verification that all of this stuff is actually working, in the form of a passing example:
$ spec spec/box_spec.rb . Finished in 0.009651 seconds 1 example, 0 failures
Now, in the process of developing this spec, I actually started with code like this to make sure I could get a failure:
it "should exist" do
violated "but doesn't"
end
However, I'll leave this as an exercise to the paranoid. For those who are quickly bored by the boiler plate, we'll jump right into Box specs now and kick up the velocity with each iteration.
Start with what you know
If you've ever found yourself delighted getting all the answers to Jeopardy one night, only to remember later that evening it was a rerun from last week, get ready to feel that way again.
The key to starting any BDD project is to begin with the most obvious assumptions, and then build your way outward and upward from there. This goes for both your specs and your implementations.
We'll start with a trivial assumption. Boxes should return some sort of collection of edges, and there should be four of them.
Here's our trivial example, which states this expectation:
(2) spec/box_spec.rb
it "should have 4 edges" do @box.edges.size.should == 4 end
My implementation might surprise you:
(2) lib/dots/box.rb
module Dots
class Box
def edges
[nil]*4
end
end
end
Why would we write such a mindless implementation? The answer is simple. Anything more complicated would be assuming more than what we've specified. Of course, this code won't get us far, passing or not, so we add more specs.
(3) spec/box_spec.rb
[:north, :south, :east, :west].each do |dir|
it "should have an #{dir} edge" do
@box.edges[dir].should_not be_nil
end
end
As you can see, this exposes a little more about what we're expecting. It now seems like edges should be a hash-like object keyed by compass location of the edge. We haven't defined what an edge is yet, which is why we don't expect more than that for these keys; the objects returned by the edges collection shouldn't be nil.
It's worth taking a quick moment to notice that we're actually dynamically building examples here. All four of these examples will be run regardless of whether some of them fail along the way. This comes in handy because it makes it immediately apparent where problems lie, and also results in nice output:
$ spec spec/box_spec.rb -f s A dots box - should exist - should have 4 edges - should have an north edge - should have an south edge - should have an east edge - should have an west edge
Being able to dynamically generate examples lets you keep your expectations simple, keeping things well organized into individual scenarios.
Let's take a look at the implementation that makes the above examples pass:
(3) lib/dots/box.rb
module Dots
class Box
def edges
Hash[:north, true, :south, true, :east, true, :west, true]
end
end
end
Not surprisingly, it's a Jeopardy rerun all over again. But if you're feeling the strain of writing such code, it's only because you're looking at it after the fact. In practice, I flew through the first few iterations of this application in only a few moments, using them to form what you might jot down on scratch paper, or work through in your head otherwise. I tend to trust code more than I do my own ideas of what might work, so this workflow fits wonderfully for me. If you're new to this technique, it might take some time, but BDD really does help you to think in code.
Still, some of you might be itching to take a look at some code that actually does something. Things start to get interesting around iteration 6, so let's look at the full spec there:
(6) spec/box_spec.rb
require File.join(File.expand_path(File.dirname(__FILE__)),"helper")
require "#{LIB_DIR}/box"
def directions
[:north,:south,:east,:west].each { |dir| yield(dir) }
end
describe "A dots box" do
before :each do
@box = Dots::Box.new
end
it "should have 4 edges" do
@box.edges.size.should == 4
end
directions do |dir|
it "should have an #{dir} edge" do
@box.edges[dir].should_not be_nil
end
it "#{dir} edge should be :not_drawn by default" do
@box.edges[dir].should == :not_drawn
end
it "#{dir} edge should be :drawn when draw_edge(#{dir.inspect}) is called" do
@box.draw_edge(dir)
@box.edges[dir].should == :drawn
end
end
it "should return nil for owner() by default" do
@box.owner.should be_nil
end
end
describe "An incomplete dots box" do
before :each do
@box = Dots::Box.new
end
it "should return false for completed?" do
@box.should_not be_completed
end
it "should not allow an owner to be set" do
lambda { @box.owner = "Gregory" }.should raise_error(Dots::BoxIncompleteError)
end
end
describe "A completed dots box" do
before :each do
@box = Dots::Box.new
directions { |dir| @box.draw_edge(dir) }
end
it "should return true for completed?" do
@box.should be_completed
end
it "should allow an owner to be set" do
@box.owner = "Gregory"
@box.owner.should == "Gregory"
end
end



