RSpec Mocks for the Sad and Desperate
No matter how good I think I am at something, I try to remember that I’m always learning and I can always be better. This is especially true with all things technological, whether it’s server or code, and one of my favorite things to suck less at is RSpec. When I first started working with Ruby and Rails, I was mystified by it. Written poorly, a spec quickly turns into a mess of muddled expectations and shoulds, vague statements in large blocks that can require more management than the code it claims to prove; written well, it’s a dream, a revolution, a safety net and documentation all in one.
Lately, I’ve been working on leveling up my specs quite a bit. It’s sort of embarrassing to say but I had a really hard time understanding exactly how or why someone would use mocks within a spec. I mean, I understood the concept — stand-ins for objects and methods to ensure you are only testing a small subset of your code, not related objects or methods — but I found the example code somewhat hard to read. It wasn’t until I committed a little time and had to start writing my own that I realized it isn’t difficult at all, it’s just that most of the resources out there that attempt to explain them do a crappy job.
So here’s the deal: a double is a dummy object that acts as a stand-in for a real object. I’d say to think of it like a stunt double but it’s more of a crash test dummy, a mannequin object without methods of its own. It’s a shell, a placeholder, and you use stubs to create its methods and responses.
Let’s pretend we want to test a method, `get_score`. This method exists in a mixin and performs some sort of magic on User objects to return the user’s score. The thing is, since your module doesn’t actually retrieve the score, you don’t want to test anything on User, you just want to make sure that the proper call is made. To do that, you create a double to stand in for a user.
describe MyModule::ScoreGetter do let(:clazz) do Class.new do include MyModule::ScoreGetter #contains the get_score method end end describe 'method get_score' do let(:user) { double("a user object") } let(:obj) { clazz.new } it 'returns a score' do expect(obj.get_score(user)).to eq 50 end end end
Note that we created a double to represent a user instead of calling User.new. Easy so far, but now what? Now we start stubbing some methods.
RSpec’s documentation says a stub is “an instruction to an object (real or test double) to return a
known value in response to a message.” In practice, that really just means that a stub is a stand-in for calling any actual method. You write the expected response of a method call rather than actually calling the method. WHY, though? Imagine our code for `get_score` looks something like this:
module MyModule module ScoreGetter def get_score(user) complicated_private_method(user) end private def complicated_private_method(user) #all sorts of wacky shit, it's preparing stuff on your calling object or something, ultimately ending in... user.score_generation(self) end end end class User #code def score_generation(caller) #sophisticated score generation algorithm end end
We stub because we don’t want to test `score_generation`. It goes outside the scope of this unit test, maybe it involves a database and we don’t want to perform queries, maybe it’s slow, maybe it uses someone else’s code and it’s unstable. If it fails, your `get_score` method will fail, even if `get_score` isn’t broken! All we want to test is:
- our class calls `get_score`
- `complicated_private_method` calls `user.score_generation` and returns a score
That’s it. Doing this is easy.
it 'returns a score' do expect(user).to receive(:score_generation).with(obj).and_return(50) expect(obj.get_score(user)).to eq 50 end
You are expecting `user` to receive the method `score_generation` with `obj` as a parameter and you decided it will return `50`. The syntax here might be a little misleading: you aren’t expect it to return 50, you are saying IT WILL RETURN 50. Your expectation is on the method and its parameter.
Alternatively, if you don’t want to declare at as an expectation and just want a single expectation for your test, you can use the `stub` method with a very similar syntax.
it 'returns a score' do user.stub(:score_generation).and_return(50) expect(obj.get_score(user)).to eq 50 end
All we’re saying is, “when the user double receives a call to `score_generation`, return 50.” This is not an expectation, though, so your spec won’t fail if this doesn’t happen unless another expectation relies on it. In other words, we could have this:
it 'returns a score' do user.stub(:score_generation).and_return(50) user.stub(:do_this_thing).and_return(:foo) expect(obj.get_score(user)).to eq 50 end
…and your spec would still pass because `do_this_thing` is acting as a double’s method definition, not an expectation. Change that to `expect` and your spec’s passing depends on it.
Again, we have to do this because our double doesn’t have a `score_generation` method and even if it did, we wouldn’t want it to actually be called because we don’t want to rely on that class or its damned dirty methods. An important thing to remember is to declare your stubs before the method is actually called; if you don’t, the method will be called before the stub is in place and it won’t know how to behave.
For me, one of the trickier parts of working with this stuff comes when I’m looking at someone else’s code and they’re using a lot of stubs to unfamiliar methods. Effective stubbing expects you to understand the flow of messages within the code. There have been occasions where I’ll add or modify a method and not have the test show the expected change, only to discover that a method further up the chain has been stubbed and my method isn’t actually being called! Read carefully, understand completely.
That’s it for now. Hopefully this will help someone along!