advertisement

Print

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.

Pages: 1, 2, 3, 4, 5

Next Pagearrow