Quick Walkthrough of RSpec Mocks (Introduction) in Code

Before we go through a lot of code samples, make sure you understand the different testing terms like mocks, stubs and test doubles. That’ll help you understand this article much better as I use a lot of those terminologies while going through different code samples.

A test double is a generic term (for stubs and mocks) that represents a real object (but sort of fake) to which messages can be passed (method calls) and fake return values can be specified. It’s used in unit testing to test a particular system or object in isolation. In this article we’ll go through test doubles (mocks) in RSpec. Let’s see how to create a test double representing the object being faked.

What's the one thing every developer wants? More screens! Enhance your coding experience with an external monitor to increase screen real estate.

dbl = double('post') # accepts an optional identifier
dbl.some_method # fails with an exception

Why did it fail ? Because test doubles are “strict” by default. Any method called on the double that hasn’t been “allowed” or “expected” will throw an error. So what should be done instead ? There are a couple of ways – allow or expect.

Allowing Messages

We can allow the reception of messages (method calls) on a test double to avoid failure.

dbl = double('post')

# Allowing Messages
allow(dbl).to receive(:some_method)
dbl.some_method # returns nil

# Allow Messages in bulk (with return values specified)
allow(dbl).to receive_messages(:foo => 10, :bar => 20)
dbl.foo # 10
dbl.bar # 20

# A short hand for the entire above code can be this
dbl = double('post', :foo => 10, :bar => 20)
dbl.foo # 10
dbl.bar # 20

Expecting Messages

Let’s also look into Expected messages which is a little different from allowing:

dbl = double('post')
# Expect the passing of some_method to dbl
expect(dbl).to receive(:some_method) { 10 }

dbl.some_method # HAS to be called else will fail with error

# Similarly we also have expect(dbl).not_to
expect(dbl).not_to receive(:some_method)

dbl.some_method # will trigger error since it wasn't supposed to be called

This way of expecting message is basically the concept of “mocking” where you set a message expectation.

Note: When you’ve two it blocks, the allow() or expect() mocks/stubs in one will not affect the one in another. Mocking/Stubbing on a test double is specific to the it block in which they’re defined or basically they’re cleaned after each example/spec.

Partial Test Doubles

Oh this is really simple. You have a real object in the system and want to fake a method on it. The object could be a class itself (faking class methods) or an instance of a class (faking instance methods). Let’s see a really simple example that should clarify everything.

# Foo is a real class with a couple of methods
foo = Foo.new

# There's an instance method called Foo#some_method that returns 10
expect(foo).to receive(:some_method).and_return('something')
foo.some_method # 'something', not 10

expect(foo).to receive(:inexistent_method) # perfectly valid
foo.inexistent_method # nil

This is same as the vanilla expect() use obviously, but the idea is you can expect something on an existing real object (in the system) and override some of its existent methods too.

Non-Strict (Loose) Test Doubles

By default test doubles are strict, i.e., errors will be raised if you try to call a method (pass a message) that has not been allowed or expected on the test double. They can be made “loose” wherein when a random method that was not allowed or expected is called, it’ll return the test double object itself. Some code in action will clear things up:

dbl = double()
dbl.test # raise error cuz not allow()'ed or expect()'ed

# We'll make it loose now!
dbl = double('some identifier', :foo => 10).as_null_object
dbl.test # return the test double
dbl.foo # return 10
expect(dbl.test).to be(dbl) # will pass
expect(dbl.foo).to eq(10) # will pass

Spies

In our examples we put expectation in the beginning and then passed the message (called the method being mocked). The other way to do something similar (which is an approach that a lot of people prefer) is using Spies. Let’s see a simple example.

foo = spy('foo')
foo.some_method
expect(foo).to have_received(:some_method)

So the key is to use have_received() after your mock method has been triggered unlike our previous case where the method was triggered after our expectation.

Scope

Mocks and Stubs (test doubles) have a per example lifecycle, i.e., they get cleaned up after each example/spec. So if you want to reuse them, then before hooks can help. More information here, but a super basic example is this:

# This hook runs before every example
before(:example) do
  allow(MyClass).to receive(:foo)
end

Verifying Doubles

We just went through normal doubles where we quickly create a double and start expecting or allowing methods on it as well as specify the return values. There are a couple of different types of doubles that are also known as “verifying doubles” that will check whether the methods being stubbed or mocked are actually present on the underlying object making it way more stricter than normal test doubles. Let’s see them in code:

Instance Doubles

These are created using instance_double() to which we pass a class name or an object as the first argument. Then when any method is mocked or stubbed, RSpec makes sure that the expected/allowed method actually exists on the instance of the class/object passed. Note you cannot pass it an existing instance but an object (classes are objects in Ruby for instance) that can be instantiated. Along with the existence of the method, argument verification also happens.

# Instance Doubles
foo = instance_double(Foo)
# will throw error if Foo doesn't have an instance method called `bar`
allow(foo).to receive(:bar)

Class Doubles

They’re similar to instance_double(), but the difference is that this works only on class (or even module) methods (methods defined using self).

# Class Doubles
foo = class_double('Foo')
allow(foo).to receive(:bar) # bar has to be a class method

Object Doubles

The concept of object double is pretty much similar to instance or class doubles except for the fact that you pass an existing “template” object to it on which stubbing and mocking is done.

foo = object_double(some_existing_object)
allow(foo).to receive(:bar) # bar must be an existing method on some_existing_object

You might think this is the same as an instance double, but the difference is, you can pass an existing object which could actually be an instance, to object_double() whereas instance_double() will accept an object (like class) that can be instantiated. That might seem confusing but it’s basically an instance (object) vs class (that can be instantiated).

Partial Doubles

We already discussed partial test doubles before, this section is basically about the fact that by default partial doubles are not verified. You can enforce verification in terms of method existence and arguments passed/allowed just like an object_double().

Personally, I find the usage of an object_double() and a partial double quite similar.

Setting Constraints

A lot of different types of constraints can be set when mocking by expecting. We’ll see a couple of different examples in code but feel free to visit the docs for a comprehensive guide.

dbl = double()

# When calling dbl.sum will have to pass 10, 'ten' and false as args
expect(dbl).to receive(:sum).with(10, 'ten', false)
# Also something like this
expect(dbl).to receive(:sum).with(10, any_args, boolean)
# dbl.sum 10, { foo: 'bar' }, true

# More: https://relishapp.com/rspec/rspec-mocks/v/3-2/docs/setting-constraints/matching-arguments


# Mock expects dbl.meth call at least once
# and max 4 times
expect(dbl).to receive(:meth).at_least(:once)
expect(dbl).to receive(:meth).at_most(4).times

# More: https://relishapp.com/rspec/rspec-mocks/v/3-2/docs/setting-constraints/receive-counts


# Expect method calls in a specific order
expect(dbl).to receive(:meth1).ordered
expect(dbl).to receive(:meth2).ordered
expect(dbl).to receive(:meth3).ordered

dbl.meth1
dbl.meth2 # If this is called before meth1 or after meth3 then test will fail
dbl.meth3

# More: https://relishapp.com/rspec/rspec-mocks/v/3-2/docs/setting-constraints/message-order

Stubbing and Hiding Constants

There might be times when you’d want to stub a constant which will last for the lifecycle of the test, i.e., just like method stubs, stubbed constants will also be restored to their original state when the spec completes. This works for both defined constants as well as undefined constants and remember that the constant names must be fully qualified:

# The constant you pass can either be a real
# existing one in the system or an unreal undefined one

stub_const('Foo::Bar', 'bar constant')
p Foo::Bar # "bar constant"

It is also possible to sort of hide or uninitialize a constant for the duration of a test:

# The constant you pass

p Foo::Bar # "test constant"
hide_const('Foo::Bar')
p Foo::Bar # Should raise a NameError

Sometimes coders also hide an undefined constant where the test might either run in isolation or in a full environment where the constant might be defined and loaded in the environment.

Setting Responses

There are multiple different ways to set a response when allowing or expecting messages. Let’s see a couple of them in code along with how to set expectations for them:

# Return a value
allow(dbl).to receive(:foo).and_return(10)
expect(dbl.foo).to eq(10)

# Return multiple values in an order, in the end keep returning the last value
allow(dbl).to receive(:foo).and_return(1, 2, 3)
expect(dbl.foo).to eq(1)
expect(dbl.foo).to eq(2)
expect(dbl.foo).to eq(3)
expect(dbl.foo).to eq(3)
expect(dbl.foo).to eq(3)


# Raise an error
allow(dbl).to receive(:foo).and_raise(NameError)
expect { dbl.foo }.to raise_error(NameError) # Not the use of a block with expect


# Throwing Symbols (Just like raising an error)
allow(dbl).to receive(:foo).and_throw(:test, 'message')
arg = catch :foo do
  dbl.foo # should throw from here
  # shouldn't get here
end
expect(arg).to eq('message')


# Yield arguments
allow(dbl).to receive(:foo).and_yield(10)
a = nil
dbl.foo { |x| a = x }
expect(a).to eq(10) # will pass


# Call original implementation
allow(Foo).to receive(:bar).and_call_original # Original Foo.bar does a sum
expect(Foo.bar(10, 20)).to eq(30) # Will call the original Foo.bar, not the mocked one


# Block implementation
# You can pass blocks as stubs
allow(dbl).to receive(:foo) do |arg|
  expect(arg).to eq('bar') # pass
  10 # return value
end
response = dbl.foo 'bar'
expect(response).to eq(10) # pass


# Wrapping the original implementation
# this only works with partial doubles as they're
# real representation of the object/class (collaborator)
expect(Foo).to receive(:sum).and_wrap_original { |m, *args| m.call(*args) }
# Now when Foo.sum is called, the original implementation will
# be called instead of the mock (Foo.sum expectation)
expect(Foo.sum(2,3)).to eq(5) # will pass

That’s all for now!

Reference:

Author: Rishabh

Rishabh is a full stack web and mobile developer from India. Follow me on Twitter.

1 thought on “Quick Walkthrough of RSpec Mocks (Introduction) in Code”

  1. Hi,

    Found an interesting thing that need to be considered while using class_double for stubbing class methods i.e; class_double doesn’t actually replace the constant. We have to call as_stubbed_const method to replace the original example :-
    ———————————————————————————–
    foo = class_double(“Foo”)
    expect(foo).to receive(:bar).and_return(“Hello”)
    xyz = Foo.bar # executes the bar method in Foo class
    xyz = foo.bar # returns “Hello” with out executing the bar method of Foo class
    ———————————————————————————–
    foo = class_double(“Foo”).as_stubbed_const
    expect(foo).to receive(:bar).and_return(“Hello”)
    xyz = Foo.bar #returns “Hello” with out executing the bar method of Foo class
    xyz = foo.bar #returns “Hello” with out executing the bar method of Foo class

    Relevant StackOverflow thread – http://stackoverflow.com/q/26202116

Leave a Reply

Your email address will not be published. Required fields are marked *

*