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
Adding a Surrogate Console Outputter
Like last time, we first need to create the surrogate and define the methods we expect it to have.
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.
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?
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.
Now, when using it, we can simply set the number of remaining generations on the board and then call run.
Summary
So, what have we learned between the two parts?
- We learned that most uses of RSpec dynamic mocks to simply stub attributes can be easily converted to using Surrogate.
- The use of Surrogate provides more immediate and specific feedback when your mocks start diverging from their real counterparts.
- While not as easy as converting simple stubs of attributes, most message expectations can be easily converted to use Surrogate.
- 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.
- 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.