Behavior Driven Development Using Ruby (Part 3)
Pages: 1, 2, 3, 4, 5
A quick run to generate the spec docs shows that each context is now also verifying its shared behaviors:
$ spec spec/box_spec.rb -f s
An incomplete dots box
- should have 4 edges
- should have an north edge
- should have an south edge
- should have an east edge
- should have an west edge
- should return nil for owner() by default
- should return false for completed?
- should not allow an owner to be set
- north edge should be :not_drawn by default
- north edge should be :drawn when draw_edge(:north) is called
- south edge should be :not_drawn by default
- south edge should be :drawn when draw_edge(:south) is called
- east edge should be :not_drawn by default
- east edge should be :drawn when draw_edge(:east) is called
- west edge should be :not_drawn by default
- west edge should be :drawn when draw_edge(:west) is called
A completed dots box
- should have 4 edges
- should have an north edge
- should have an south edge
- should have an east edge
- should have an west edge
- should return nil for owner() by default
- should return true for completed?
- should allow an owner to be set
- should not allow an owner to be set more than once
A positioned dots box at (0,0)
- should have 4 edges
- should have an north edge
- should have an south edge
- should have an east edge
- should have an west edge
- should return nil for owner() by default
- should have generated line coordinate tuples by compass direction
- should give :north for edge?(#<Set: {[1, 1], [0, 1]}>)
- should give false for edge?(#<Set: {[10, 1], [10, 2]}>)
- should give :south for edge?(#<Set: {[0, 0], [1, 0]}>)
- should give :east for edge?(#<Set: {[0, 0], [0, 1]}>)
- should give :west for edge?(#<Set: {[1, 1], [1, 0]}>)
This technique can really come in handy for making your code a bit more DRY, and making it easy to ensure that common behaviors that span different contexts are actually checked.
You can actually include several shared behaviors in a given context, and also include shared behaviors inside other shared behaviors, so this technique will scale to arbitrary complexity.
Clarifying Examples with Custom Matchers
Those who've written a fair bit of nontrivial Test::Unit code have probably created some custom assertions to clean up their code. It is possible to do the same thing in RSpec, via custom matchers.
Let's take a quick look at some of the code from our Dots::Box spec, where we're writing some reasonably ugly specs:
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
Though it's not used in a ton of places, it'd be nice to say something like:
@box.edges[dir].should be_drawn
We'd want this code to expand out to mean the same thing essentially as our original spec, but have more meaningful error messages and descriptions. By creating a simple object with a few hooks and then creating a Kernel method that initializes a matcher for us, we can do exactly that:
class BeDrawn
def matches?(edge)
@edge = edge
@edge == :drawn
end
def description
"be drawn"
end
def failure_message
"expected edge to be drawn but wasn't"
end
def negative_failure_message
"edge was drawn but wasn't expected to be"
end
end
def be_drawn
BeDrawn.new
end
With this code loaded, we can now write our specs like this:
directions do |dir|
it "#{dir} edge should not be drawn by default" do
@box.edges[dir].should_not be_drawn
end
it "#{dir} edge should be drawn when draw_edge(#{dir.inspect}) is called" do
@box.draw_edge(dir)
@box.edges[dir].should be_drawn
end
end
Though this does reduce granularity of the tests a little bit (I'm no longer checking for :not_drawn), it actually captures the expected behavior a little better than our original code. It also lets me re-use this code as needed and only need to change the underlying comparison in one place if the underlying implementation changes.
Of course, we can actually go a little bit farther if we create a matcher with an argument:
class HaveDrawnEdge
def initialize(direction)
@direction = direction
end
def matches?(box)
box.edges[@direction] == :drawn
end
def description
"have #{@direction} edge drawn"
end
def failure_message
"expected #{@direction} edge drawn but wasn't"
end
def negative_failure_message
"#{@direcion} edge should not have been drawn"
end
end
def have_drawn_edge(dir)
HaveDrawnEdge.new(dir)
end
We can now write specs that are really expressive, abstracting our core matching code even more:
directions do |dir|
it "#{dir} edge should not be drawn by default" do
@box.should_not have_drawn_edge(dir)
end
it "#{dir} edge should be :drawn when draw_edge(#{dir.inspect}) is called" do
@box.draw_edge(dir)
@box.should have_drawn_edge(dir)
end
end
As systems get bigger, custom matchers become a very handy way to avoid large and ugly looking examples that are prone to maintenance headaches. Since they're literally basic Ruby objects that just have a certain interface they need to implement, they're very simple to work with and can easily be stashed away in a helper file to be used across your project's specs.
Though there are lots more tricks to learn in RSpec, it's worth taking the time now to talk about a few well integrated third party tools that help you make sure your specs are doing what you think they are.



