Migrating from RSpec Dynamic Mocks to Surrogate - Part 1

Migrating from RSpec Dynamic Mocks to Surrogate - Part 1

Eric Meyer

January 13, 2013

Here's the situation: You use RSpec. You like RSpec. But you want more confidence from your tests than you get with RSpec's dynamic mocks while continuing to use mocks. The answer: Surrogate.

For the uninitiated, Surrogate is a gem that aids in the creation of hand-rolled mocks and can used with any testing library. At the moment the "surrogate" gem ships with RSpec matchers for making assertions about your mocks, but it is likely that they will be pulled out into their own gem called "surrogate-rspec" at some point in the future. RSpec's argument matchers such as anything, no_args, and hash_including are also available to use with Surrogate.

How Does it Work?

In short, you have one place that defines the contract for the mock, and one test that makes sure the object the mock represents implements the methods defined by the mock. Failure to implement all the methods defined on the surrogate or attempting to use the surrogate incorrectly, by attempting to use methods that are not defined on it, will raise errors. The complexity of a surrogate can range from a data structure of attributes up to redefining methods to have their own behavior.

Enough of talking about Surrogate. This blog is supposed to be about migrating to Surrogate, so let's look at some code! Since Global Day of Code Retreat was fairly recently, I decided to convert some tests for Conway's Game of Life that I wrote that day, originally using RSpec dynamic mocks. I've only included parts of the codebase in these examples, but the full implementation can be found here on Github.

Conway's Game of Life - A Quick Explanation

Gosper's Glider Gun
Image taken from [Wikipedia](http://en.wikipedia.org/wiki/Conway%27s_Game_of_Life)

Feel free to skip this section if you are already familiar with Conway's Game of Life.

Conway's Game of Life is a cellular automaton, determined solely by its initial state. You have a infinitely-sized, two-dimensional grid of cells that each have one of two possible states, alive or dead. During each step in the game, all cells simultaneously change their state based on the following rules.

  1. Any living cell with fewer than two living neighbors dies, due to underpopulation.
  2. Any living cell with 2 or 3 living neighbors stays alive.
  3. Any living cell with greater than 3 living neighbors dies, due to overpopulation.
  4. Any dead cell with exactly 3 living neighbors is brought to life.

Migrating a Simple Example - Stubbing Attributes

Using RSpec Dynamic Mocks

Here we have some tests using RSpec dynamic mocking to simply stub attributes. They create a mock cell which is used to determine the state of the cell in the next generation.

describe GameOfLife::Rules do
		context "with a living cell" do
				it "dies with 1 living neighbor" do
						cell = mock('Cell', alive?: true, number_of_living_neighbors: 1)
						GameOfLife::Rules.should_not be_alive_in_next_generation(cell)
				end
				it "stays alive with 2 living neighbors" do
						cell = mock('Cell', alive?: true, number_of_living_neighbors: 2)
						GameOfLife::Rules.should be_alive_in_next_generation(cell)
				end
		end
		# Additional Tests Omitted...
end

Here is the code making those tests pass.

module GameOfLife
		class Rules
				def self.alive_in_next_generation?(cell)
						case cell.number_of_living_neighbors
						when 2
								cell.alive?
						when 3
								true
						else
								false
						end
				end
		end
end

Converting these tests to use Surrogate is really easy with the recent addition of the ability to initialize mocks with a hash of attributes.

Creating the Surrogate Cell

First, we must create the surrogate. Surrogate.endow(self) gives us the ability to configure the surrogate. Calling define adds a method to the Surrogate along with some helpers to configure its behavior and set up expectations.

class MockCell
		Surrogate.endow(self)
		define(:alive?) { false }
		define(:number_of_living_neighbors) { 0 }
end

Now, when we create instances of MockCell in our tests, we can only set expectations or define behavior for methods or attributes created via define. We will go into more detail later about how this works.

Using the Surrogate Cell

Next, instead of calling mock('Cell', hash_of_initial_attributes), we simply call MockCell.factory(hash_of_initial_attributes). Depending on how consistent you are with creating your RSpec mocks (and how clever you are with regular expressions), this might actually be a simple search and replace.

describe GameOfLife::Rules do
		context "with a living cell" do
				it "dies with 1 living neighbor" do
						cell = MockCell.factory(alive?: true, number_of_living_neighbors: 1)
						GameOfLife::Rules.should_not be_alive_in_next_generation(cell)
				end
				it "stays alive with 2 living neighbors" do
						cell = MockCell.factory(alive?: true, number_of_living_neighbors: 2)
						GameOfLife::Rules.should be_alive_in_next_generation(cell)
				end
		end
		# Additional Tests Omitted...
end

Also, I will often extract something like the cell creation into a single method. That way, if the way I create my cell changes, as it did here, I will only have to make the change in one place. That would have made our work to convert the previous tests a lot simpler, however I wanted to keep these examples close to what they looked like when I first created them.

Mixing Surrogate with RSpec

It is also perfectly valid to use both types of mocks in a project, even within a single test, which is potentially useful when migrating a bunch of tests over, so you can go one test or one object at a time.

describe GameOfLife::Rules do
		context "with a living cell" do
				it "dies with 1 living neighbor" do
						cell = MockCell.factory(alive?: true, number_of_living_neighbors: 1)
						GameOfLife::Rules.should_not be_alive_in_next_generation(cell)
				end
				it "stays alive with 2 living neighbors" do
						cell = mock('Cell', alive?: true, number_of_living_neighbors: 2)
						GameOfLife::Rules.should be_alive_in_next_generation(cell)
				end
		end
		# Additional Tests Omitted...
end

Now the tests are all passing using the surrogate, but what has this bought us? The last step is to write a spec for the "real" cell class, likely called Cell.

describe Cell do
		it "implements the methods defined in MockCell" do
				MockCell.should be_substitutable_for(Cell)
		end
end

To see the benefit of Surrogate, we'll take a look at some instances where tests using the MockCell would correctly fail when RSpec's dynamic mocks would not.

What happens when Cell and MockCell don't match?

If you run the above test, with a Cell that looks like this:

class Cell
		# Empty class definition
end

You will get a test failure like this:

Failures:

 1) Cell implements the methods defined in MockCell
 Failure/Error: MockCell.should substitute_for(Cell)
 Was not substitutable because surrogate has extra instance methods: [:alive?, :number_of_living_neighbors]
 # ./spec/game_of_life/rules_spec.rb:15:in `block (2 levels) in <top (required)>'

What happens when I use MockCell inappropriately?

This can happen for two main reasons:

  1. One or more of the methods on Cell change, which forces a change in MockCell, but not all tests using MockCell are updated.
  2. You make a mistake when writing a test using the MockCell.

For example, when you write a test looking like this:

describe GameOfLife::Rules do
		context "with a living cell" do
				it "dies with 1 living neighbor" do
						# Passing in "alive" instead of "alive?"
						cell = MockCell.factory(alive: true, number_of_living_neighbors: 1)
						GameOfLife::Rules.should_not be_alive_in_next_generation(cell)
				end
		end
		# Additional Tests Omitted...
end
Failures:

 1) GameOfLife::Rules with a living cell dies with 1 living neighbor
 Failure/Error: cell = MockCell.factory(alive: true, number_of_living_neighbors: 1)
 Surrogate::UnknownMethod:
 doesn't know "alive", only knows "alive?", "number_of_living_neighbors"
 # ./spec/game_of_life/rules_spec.rb:25:in `block (3 levels) in <top (required)>'

To Be Continued...

This blog was only Part 1. Part 2 will contain more complicated examples of migrating tests using message expectations with expected arguments and creating a surrogate to use in place of a state based object with complex behavior.