advertisement

Print

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

Pages: 1, 2, 3, 4

Next Pagearrow