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
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.
- Any living cell with fewer than two living neighbors dies, due to underpopulation.
- Any living cell with 2 or 3 living neighbors stays alive.
- Any living cell with greater than 3 living neighbors dies, due to overpopulation.
- 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.
Here is the code making those tests pass.
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.
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.
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.
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
.
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:
You will get a test failure like this:
What happens when I use MockCell inappropriately?
This can happen for two main reasons:
-
One or more of the methods on
Cell
change, which forces a change inMockCell
, but not all tests usingMockCell
are updated. -
You make a mistake when writing a test using the
MockCell
.
For example, when you write a test looking like this:
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.