Migrating from RSpec Dynamic Mocks to Surrogate - Part 2

This blog is Part 2 of a series of blogs about migrating to Surrogate. Part 1 can be found here. If you are unfamiliar with Surrogate and haven't read Part 1, I recommend reading it before this blog.

To recap, Part 1 contained a introduction to both the problem and to Surrogate, along with an example of migrating an RSpec mock used for stubbing attributes.

Part 2 will contain more complicated examples including how to migrate mocks using message expectations and how to mock out methods that affect the return value of a different method. We will continue using Conway's Game of Life for our examples.

Let's jump right into some code!

Migrating Message Expectations With Expected Arguments

The example we are going to migrate over is one with message expectations with arguments. In other words, tests using should_receive. The section of code we are looking at is the main game loop for Conway's Game of Life.

The runner takes an instance of the Board and an outputter, both of which are going to be represented using mocks. As long as there are living cells on the board, the outputter shows the board, and then the board advances to the next generation.

The Specs Using should_receive

module GameOfLife
  describe Runner do

    before(:each) do
      @outputter = mock('ConsoleOutputter', show_board: nil)
      @board = mock('board', has_any_living_cells?: true, advance_generation: nil)
      @runner = Runner.new(@board, @outputter)
      @runner.stub(:sleep)
    end

    it "outputs the board" do
      @board.stub(:has_any_living_cells?).and_return(true, false)
      @outputter.should_receive(:show_board).with(@board)
      @runner.run
    end

    it "advances the board's generation" do
      @board.stub(:has_any_living_cells?).and_return(true, false)
      @board.should_receive(:advance_generation)
      @runner.run
    end

    it "pauses between loops" do
      @board.stub(:has_any_living_cells?).and_return(true, false)
      @runner.should_receive(:sleep).with(Runner::TIME_BETWEEN_GENERATIONS)
      @runner.run
    end

    it "shows the board until there are no living cells" do
      @board.stub(:has_any_living_cells?).and_return(true, true, false)
      @outputter.should_receive(:show_board).exactly(2).times
      @runner.run
    end

  end
end

Adding a Surrogate Console Outputter

Like last time, we first need to create the surrogate and define the methods we expect it to have.

require "surrogate/rspec"

class MockOutputter
  Surrogate.endow(self)
  define(:show_board) { |board| }
end

To convert the test that asserts that show_board was called, you simply move the assertion to after @runner.run and change the matcher from should_receive to should have_been_told_to. Here it is also easy to migrate tests over one at a time, since should_receive still works on surrogate objects, even though that defeats the purpose of using Surrogate. Message expectations set up using should_receive will not ensure that the method exists.

module GameOfLife
  describe Runner do

    before(:each) do
      @outputter = MockOutputter.factory
      @board = mock('board', has_any_living_cells?: true, advance_generation: nil)
      @runner = Runner.new(@board, @outputter)
      @runner.stub(:sleep)
    end

    it "outputs the board" do
      @board.stub(:has_any_living_cells?).and_return(true, false)
      @runner.run
      @outputter.should have_been_told_to(:show_board).with(@board)
    end

    it "advances the board's generation" do
      @board.stub(:has_any_living_cells?).and_return(true, false)
      @board.should_receive(:advance_generation)
      @runner.run
    end

    # Additional Tests Omitted...
  end
end

One nice thing to notice is that we don't have to explicitly stub show_board for the tests that don't care about it. This is because we set the default behavior of show_board on MockOutputter by passing in a block when we called define. Passing in the block is optional, and passing no block when calling define is the same as passing in an empty block yielding nothing.

What happens when I set an expectation on an undefined method?

module GameOfLife
  describe Runner do

    before(:each) do
      @outputter = MockOutputter.factory
      @board = mock('board', has_any_living_cells?: true, advance_generation: nil)
      @runner = Runner.new(@board, @outputter)
      @runner.stub(:sleep)
    end

    it "outputs the board" do
      @board.stub(:has_any_living_cells?).and_return(true, false)
      @runner.run
      @outputter.should have_been_told_to(:show_THE_board).with(@board)
    end

    # Additional Tests Omitted...
  end
end
Failures:

  1) GameOfLife::Runner outputs the board
     Failure/Error: @outputter.should have_been_told_to(:show_THE_board).with(@board)
     Surrogate::UnknownMethod:
       doesn't know "show_THE_board", only knows "show_board"
     # ./spec/game_of_life/runner_spec.rb:17:in `block (2 levels) in <module:GameOfLife>'

Taking Advantage of Hand-Rolled Mocks

Converting the Board to a Surrogate

We've finished converting the Outputter into a Surrogate, but we are still left with the board as a dynamic mock, so let's go ahead and convert that.

Rather than just do a simply doing a line-by-line conversion, let's take advantage of the fact that we are using a hand-rolled mock. With any hand-rolled mock, not just Surrogate, you have the option of giving a stubbed method its own behavior rather than simply stubbing its return value. Instead of stubbing has_any_living_cells? to return false after a certain number of true return values, let's be more expressive in our tests by giving our MockBoard some simple behavior. This MockBoard lives for a given number of generations, then is void of any living cells.

In short, we aren't stubbing out the behavior of Board, but instead are swapping Board with an object that has the same interface, but simpler behavior.

require "surrogate/rspec"

class MockBoard
  Surrogate.endow(self)
  define(:has_any_living_cells?) do
    @number_of_remaining_generations > 0
  end
  define(:advance_generation) do
    @number_of_remaining_generations -= 1
  end

  define(:bring_to_life_at) { | location| }
  define(:has_living_cell_at?) { |location| }

  attr_accessor :number_of_remaining_generations
end

Now, when using it, we can simply set the number of remaining generations on the board and then call run.

module GameOfLife
  describe Runner do

    before(:each) do
      @outputer = MockOutputter.new
      @board = MockBoard.new
      @runner = Runner.new(@board, @outputer)
      @runner.stub(:sleep)
    end

    context "with only one remaining generation" do
      before(:each) do
        @board.number_of_remaining_generations = 1
      end

      it "outputs the board" do
        @runner.run
        @outputer.should have_been_told_to(:show_board).with(@board)
      end

      it "advances the board's generation" do
        @runner.run
        @board.should have_been_told_to(:advance_generation)
      end

      it "pauses between generations" do
        @runner.should_receive(:sleep).with(Runner::TIME_BETWEEN_GENERATIONS)
        @runner.run
      end
    end

    context "with multiple remaining generations" do
      before(:each) do
        @board.number_of_remaining_generations = 5
      end

      it "shows the board for each generation" do
        @runner.run
        @outputer.should have_been_told_to(:show_board).times(5)
      end

      it "sleeps between each generation" do
        @runner.should_receive(:sleep).with(Runner::TIME_BETWEEN_GENERATIONS).exactly(5).times
        @runner.run
      end
    end

  end
end

Summary

So, what have we learned between the two parts?

  1. We learned that most uses of RSpec dynamic mocks to simply stub attributes can be easily converted to using Surrogate.
  2. The use of Surrogate provides more immediate and specific feedback when your mocks start diverging from their real counterparts.
  3. While not as easy as converting simple stubs of attributes, most message expectations can be easily converted to use Surrogate.
  4. The use of a hand-rolled mock gives us more flexibility and options when compared to a dynamic mock. Instead of simply stubbing attributes, you can create a simpler version of your real object.
  5. Surrogate is compatible with RSpec dynamic mocks, and is in fact meant to be used with RSpec, which is useful when migrating tests over or for giving Surrogate a chance in an isolated part of your application.

Hopefully, this will help those of you on the fence about using Surrogate having an easier time giving it a try on one of your projects.

Eric Meyer, Software Craftsman

Eric Meyer has recently been developing applications for the iPhone and iPad using Objective C.

Interested in 8th Light's services? Let's talk.

Contact Us