Ruby | Rspec | Mocks & Stubs

Derek Dyer
5 min readNov 16, 2017

--

I struggled with the concept of Mock vs. Stub for a little while and it wasn’t until I was in a paired programming session with Sergii Makagon that it finally clicked for me. Seeing the process of adding mocks and stubs evolve during a refactoring session helped me understand the two concepts, so I wanted to try and explain mocks and stubs in a similarly iterative way.

Vocabulary:

Double: It’s a dumb object we create that stands-in for an object we need in order to test something.

Stub: Stubs allow an object to receive messages/methods. Usually they’re Doubles but we can allow ‘real’ objects to receive messages/methods too.

Mock: Objects with expectations. Key word being expect.

Why do we need Doubles?

Doubles are cool because sometimes classes rely on other objects in order to work. Doubles make it easy to test a class’s methods without having to instantiate objects. In unit testing, we try to be as lazy as possible. We want to instantiate the bare minimum to get a test to pass. We should never care about anything else except that method we’re testing at that time.

Why do we need Stubs?

Sometimes we rely not only on objects but those objects’ methods/messages too. We can use stubs to mimic those methods/messages.

Why do we need Mocks?

Sometimes a test expects certain messages from objects. We can add these messages at runtime for that test. We can expect a method response from a mock to fulfill the tested method’s needs.

Some quick boilerplate code for context:

The code below implements a simple example of dependency injection.

user.rb:

class User
attr_reader :email, :first_name, :last_name, :phone_number
def initialize(email:, first_name:, last_name:, phone_number:)
@email = email
@first_name = first_name
@last_name = last_name
@phone_number = phone_number
end
end

identity_provider.rb (like a class interface)

class IdentityProvider
def self.authenticate(_email)
‘The self.authenticate method has not been defined for this Identity Provider 🙊’
end
end

twitter_idp.rb

class TwitterIdp < IdentityProvider
def self.authenticate(email)
“Authenitcated 🔓 using #{name.split(‘::’).last} with email:
#{email}”
end
end

salesforce_idp.rb

class SalesforceIdp < IdentityProvider
def self.authenticate(email)
“Authenitcated 🔓 using #{name.split(‘::’).last} with email: #{email}”
end
end

facebook_idp.rb

class FacebookIdp < IdentityProvider; end

session.rb

require_relative 'user'
require_relative 'identity_provider'

class Session
attr_reader :current_user
def initialize(user)
@current_user = user
end
def create(idp = SalesforceIdp)
idp.authenticate(current_user.email)
end
end

run_idp.rb

require_relative ‘session’
session = Session.new(
User.new(
email: ‘foobar@gmail.com’,
first_name: ‘Derek’,
last_name: ‘Dyer’,
phone_number: ‘5555555555’
)
)
session.create(TwitterIdp)
session.create(FacebookIdp)
session.create

Double

I want to test Session#create, so I made a file called session_spec.rb with an empty test in it.

require_relative 'session'
RSpec.describe Session do
it 'creates a new session' do
end
end

We set ‘session’ to equal the class we are testing: Session. Looking at the ‘session.rb’ file, Session needs a User object in order for Session to be initialized. Instantiating the User class is totally an option:

require_relative 'session'
RSpec.describe Session do
let(:current_user) do
User.new(
email: 'foobar@gmail.com',
first_name: 'Derek',
last_name: 'Dyer',
phone_number: '5555555555'
)
end
let(:session) { described_class.new(current_user) } describe '#create' do
it 'creates a new session' do
end
end
end

We’re instantiating the User class for what? To test the Session#create method. Let’s look at what that method needs from User to run:

In the session.rb file:

def create(idp = SalesforceIdp)
idp.authenticate(current_user.email)
end

It needs an instance of the User class (it also needs an email from User but we’ll get to that later).

let(current_user) { double('user')

That 👆 is a double using the Rspec double method. I like to think of it as a stunt double. Let’s run it with a simple test.

require_relative ‘session’
RSpec.describe Session do
let(:current_user) { double(‘user’) } let(:session) { described_class.new(current_user) } describe ‘#create’ do
it ‘creates a new session’ do
expect(session.create).to eq(
“Authenticated 🔓 using SalesforceIdp with email: #{current_user.email}”
)
end
end

#Double “current_user”> received unexpected message :email with (no args)

Ok, we’re getting somewhere. When we ran the test for Session#create we got the above error. We need the current_user’s email. We have a few options.

Stub

Definition of stub:

A method stub is an instruction to an object (real or test double) to return a
known value in response to a message.

That sounds useful. Let’s try it with: allow(current_user).to receive(:email)

require_relative 'session'
RSpec.describe Session do
let(:current_user) { double('current_user' }let(:session) { described_class.new(current_user) }describe '#create' do
it 'creates a new session' do
allow(current_user).to receive(:email)
expect(session.create).to eq(
"Authenitcated 🔓 using SalesforceIdp with email: #{current_user.email}"
)
end
end
end

It worked! We instructed the user object to respond to email and our test is passing. Also, notice how much cleaner that is than if we instantiated a User object. If we were relying on current_user.email to respond with something specifically, we could have done this:

allow(current_user).to receive(:email) { 'email@email.com' }

As it turns out, the Session#create method doesn’t really care what current_user.email returned, as long as it responded to the method.

FYI, we can also stub methods for real objects too. Stubs are not just applicable to mocks.

Mock

Remember we said a mock was like a stub but with an expectation? Let’s see what that looks like.

require_relative 'session'
RSpec.describe Session do
let(:current_user) { double('current_user' }let(:session) { described_class.new(current_user) }describe '#create' do
it 'creates a new session' do
expect(current_user).to receive(:email) { 'email@email.com' }
expect(session.create).to eq(
"Authenticated 🔓 using SalesforceIdp with email: email@email.com"
)
end
end
end

This is better, it is more specific. It’s verifying that the session.create method is calling the method the way it’s supposed to.

Verifying doubles (Bonus Round)

Most of the time you will want some confidence that your doubles resemble an existing object in your system. Verifying doubles are provided for this purpose. If the existing object is available, they will prevent you from adding stubs and expectations for methods that do not exist or that have invalid arguments.

Verifying doubles have some clever tricks to enable you to both test in isolation without your dependencies loaded while still being able to validate them against real objects.
https://relishapp.com/rspec/rspec-mocks/docs

We have a nice test. Let’s try a verifying double like this:

let(current_user) do
instance_double('User', email: 'email@email.com')
end

session_spec.rb

require_relative 'session'RSpec.describe Session dolet(:current_user) do
instance_double('User', email: 'foo@foo.com')
end
let(:session) { described_class.new(current_user) } describe '#create' do
it 'creates a new session' do
expect(session.create).to eq(
"Authenitcated 🔓 using SalesforceIdp with email: #{current_user.email}"
)
end
end

The cool thing about verifying doubles is they prevent us from adding stubs for methods that do not exist in the actual class or that have invalid arguments. So by changing user to User, we are now mocking with confidence because the verifying double is checking to see if an instance method exists AND we’re testing in isolation without instantiating a class AND we’re stubbing.

Thanks for your time!

--

--