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.

Ruby
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.

Ruby
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:

Ruby
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.

Ruby
# 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:

Ruby
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.

Ruby
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:

Ruby
# 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.

Ruby
# 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).

Ruby
# 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.

Ruby
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.

Ruby
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:

Ruby
# 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:

Ruby
# 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:

Ruby
# 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: