advertisement

Print

Behavior Driven Development Using Ruby (Part 3)
Pages: 1, 2, 3, 4, 5

Sharing Behaviors Between Contexts

It is common to find that a certain base configuration is used across several contexts, with minor tweaks in each to cover specific cases. It's also reasonably common to find that there are certain behaviors that are common to all contexts of an object.

If we take a look at our code that describes the Dots::Box class from our little game example, you can see a lot of patterns emerge between the contexts:

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     

  it "should not allow an owner to be set more than once" do
    @box.owner = "Gregory" 
    lambda { @box.owner = "Joe" }.should raise_error(Dots::BoxOwnerAlreadySetError)
  end

end                               

describe "A positioned dots box at (0,0)" do

  require "set"                  

  before :each do
    @box = Dots::Box[0,0]    
  end  

  it "should have generated line coordinate tuples by compass direction" do
    @box.lines.should == lines   
  end  

  lines.invert.merge(Set[[10,1],[10,2]] => false).each do |edge, dir|

    it "should give #{dir.inspect} for edge?(#{edge.inspect})" do
       @box.edge?(edge).should == dir
    end

  end   

end

We are using a similar setup for each context, and really, most of the behaviors we check for in our "A dots box" context are common to all the boxes we will create, and should be verified. Luckily, RSpec has the notion of shared behaviors. These constructs allow us to share our setups and examples between several contexts. With some reworking, it's easy to refactor the code above to make use of this technique:

describe "A dots box", :shared => true 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    
  end       

  it "should return nil for owner() by default" do
    @box.owner.should be_nil
  end

end  

describe "An incomplete dots box" do

  it_should_behave_like "A dots box" 

  directions do |dir|

    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 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

  it_should_behave_like "A dots box" 

  before :each do
    directions { |dir| @box.draw_edge(dir) }
  end    

  # ... examples unchanged

end                                

describe "A positioned dots box at (0,0)" do

  require "set" 

  it_should_behave_like "A dots box"                 

  before :each do
    @box = Dots::Box[0,0]    
  end  

  # ... examples unchanged 

end

Notice that we're taking advantage of the fact that we're free to make use of the nested before :each calls. In our "A completed dots box" context, we simply make some changes to the constructed @box object from our shared context ("A dots box"), while in "A positioned dots box at (0,0)" we actually replace the object entirely. This is fairly powerful, because although we're dealing with a different object, our shared examples use the same variable name, so we can be sure that a box created with Dots::Box[0,0] works the same as one created with Dots::Box.new.

Pages: 1, 2, 3, 4, 5

Next Pagearrow