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: