advertisement

Print

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

If my specs are doing their job, it should be pretty easy for you to catch up on what's going on. You can see we've gone a lot farther, defining a way to draw edges, assign ownership to a box, and even do some basic error handling. For all the behaviors described above, we end up with a beautifully simple implementation:



(6) lib/dots/box.rb
module Dots
  class Box

    def initialize
      @edges = Hash[ :north, :not_drawn, 
                     :south, :not_drawn, 
                     :east, :not_drawn, 
                     :west, :not_drawn ]
    end    

    attr_reader :edges
    attr_reader :owner

    def draw_edge(dir)
      @edges[dir] = :drawn
    end        

    def owner=(new_owner)
      raise BoxIncompleteError unless completed?
      @owner = new_owner
    end

    def completed?
      @edges.all? { |dir,status| status == :drawn }
    end

  end 

  class BoxIncompleteError < StandardError; end
end

Hopefully by seeing the results here, you'll gain a better understanding of why it is beneficial to work in such tiny steps. Because we neurotically don't add anything to the code that isn't covered by an example to our specs, we end up with very little code that isn't doing much, and absolutely no useless code. It is almost as if writing the specs first force us to write better production code, just by osmosis.

This is all pretty straightforward and simple code, of course, but before we move on to more challenging parts, let's take a quick look at some of the tricks in our specs.

You'll notice at the top of the file I've added a helper to iterate over directions, since a few of my examples use this feature.

def directions 
  [:north,:south,:east,:west].each { |dir| yield(dir) }
end

The way this is implemented, it gets defined at the top level, which means it's available everywhere. This is notably hacky, and there are generally better ways to add helper functions to your specs, which we'll show later in this article. However, this is entirely of the "keep it simple" mindset. For what we need, this gets the job done, and we can go back to it later if it gives us trouble.

The rest of the tests are fairly benign, though we do a couple of interesting things, such as make use of the be_something helper we discussed in the first part of this article in a practical way:

  it "should return false for completed?" do
    @box.should_not be_completed
  end

We also test to make sure an exception we're raising is working as expected. In RSpec, the following code is equivalent to an assert_raises call in Test::Unit:

  lambda { @box.owner = "Gregory" }.should raise_error(Dots::BoxIncompleteError)

We need to wrap the code we are testing in a Proc to prevent the error from rising up to the top level, but other than that, this code does pretty much exactly what it reads as.

Before we fast-forward to other parts of our system and get into some of the deeper topics, let's take a look about what we know about boxes:

$ spec spec/box_spec.rb -f s

A dots box
- should have 4 edges
- should have an north edge
- north edge should be :not_drawn by default
- north edge should be :drawn when draw_edge(:north) is called
- should have an south edge
- south edge should be :not_drawn by default
- south edge should be :drawn when draw_edge(:south) is called
- should have an east edge
- east edge should be :not_drawn by default
- east edge should be :drawn when draw_edge(:east) is called
- should have an west edge
- west edge should be :not_drawn by default
- west edge should be :drawn when draw_edge(:west) is called
- should return nil for owner() by default

An incomplete dots box
- should return false for completed?
- should not allow an owner to be set

A completed dots box
- should return true for completed?
- should allow an owner to be set

As you can see, our specs are already pretty handy as English documentation for our implementation. With a task like this that has pretty well defined rules, these come pretty close to comprehendable even by non-programmers.

In iterations 7-13, we continue to iron out some remaining rules about how Boxes should work, and then develop our Grid implementation. The Grid implementation doesn't show off any new RSpec or BDD tricks, so I'm not going to cover that code, but it's all in the source package if you're interested. Here's what specdocs look like, though:

An empty dots grid
- should have one box for each edge on left side
- should have one box for each edge on right side
- should have one box for each edge on top side
- should have one box for each edge on bottom side
- should have two boxes for all inner edges
- should allow connecting adjacent dots
- should throw an error when connecting non-adjacent dots

A drawn on dots grid
- should return an empty set when connect() does not complete a box
- should return a set with a box when connect() completes one box
- should return a set of two boxes when connect() completes two boxes

To keep the focus on fresh concepts, we'll make a big leap over to the controlling process code, our Game object. Here, we'll be able to take a look at how to use mock objects to keep code isolated from its dependencies, and focus on the interactions rather than the state exchange between objects.

Introducing Mock Objects via the Game Class

The purpose of our Game class is by nature one that depends on other resources to operate. It is meant to be the control class between a user interface and the underlying data models that represent the game. This is the class that will carry us through the "get input from user, make changes, update display" process. This may sound like it is a tricky thing to test, but we'll show how you can approach it in the same simple, iterative manner as we've been doing for our more isolated objects.

Although we've not shown the implementation or specs for our Grid code, you won't actually need it to follow along with our Game specs. In fact, although our Game class implementation is meant to contain a Grid object, none is actually created when we run our Game specs!

To throw yet another monkey wrench into the gears, we're actually not even going to bother building a UI until after our Game object is functioning properly. Instead, we'll be using mock objects to define a protocol that we expect the UI to implement, which we can freely add in later.

Like before, we'll start with the trivial tasks. We want to be able to get a list of players, and then populate a grid to begin a game on. Our initial examples look like this:

(13) spec/game_spec.rb
require File.join(File.expand_path(File.dirname(__FILE__)),"helper")

describe "A newly started game" do

  before :each do
    @game = Dots::Game.new
    @game.interface = mock("UI")
    mock_player_selection
    mock_prompt_for_grid_size
    @game.start
  end   

  it "should have an array of players" do 
    @game.players.should == ["Gregory","Joe"]
  end             

  it "should set the first player entered via the UI to the current_player" do
    @game.current_player.should == "Gregory" 
  end   

  it "should populate a grid from UI input" do
    @game.grid.should_not be_nil
  end

  def mock_player_selection
    @game.interface.should_receive(:get_players).and_return(["Gregory","Joe"])
  end

  def mock_prompt_for_grid_size               
    grid = mock("grid")  
    @game.interface.should_receive(:get_grid_size).and_return([8,10])            
    Dots::Grid.should_receive(:new).with(8,10).and_return(grid)
  end     

end

Pages: 1, 2, 3, 4

Next Pagearrow