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.



