RSpec: difference between mocks and stubs
- 22 October 2017
- Ruby
- RSpec: difference between mocks and stubs
Hello! I'm back from my lovely trip to San-Francisco and eager to keep writing more articles for this blog. Today we will try to figure out the difference between mocks and stubs.
Let's define what is mock and what is stub first. Since we use RSpec in this article I'll use definition from Effective Testing with RSpec 3 book:
Stub
Returns canned responses, avoiding any meaningful computation or I/O
The code looks like this:
allow(some_object).to receive(some_method).and_return(some_value)
Mock
Expects specific messages; will raise an error if it doesn’t receive them by the end of the example
Example of mock:
expect(some_object).to receive(some_method).and_return(some_value)
Mocks are all about expectations for a method to be called, whereas stubs are just about allowing an object to respond to method call with some value.
Let's use an example to dive into this concept more. We will cover the following code by specs:
class DataProcessor
Error = Class.new(StandardError)
def process(data, validator)
raise Error unless validator.valid?(data)
# simple logic to show the idea
"#{data} processed"
end
end
We have DataProcessor
class which is responding to process
method. It accepts data
and validator
arguments. If validator
returns true
for valid?
method call, the processor will add "processed" string to the end of the data
. Simple.
Let's say we want to add a spec to check if we have "processed" string at the end of the data after processing. But method process
still requires us to pass a validator. We're not interested in that validator now, so we can just stub (return canned response) valid?
method.
Let's create an empty spec for DataProcessor
:
require 'spec_helper'
describe DataProcessor do
let(:processor) { described_class.new }
end
Now we can set up a case with expectation:
require 'spec_helper'
describe DataProcessor do
let(:processor) { described_class.new }
it 'adds processed to valid data' do
expect(processor.process('foo', validator)).to eq('foo processed')
end
end
The last step we need to do is to create dummy validator
that would respond to valid?
method and return true
.
Let's create double first:
validator = double(:validator)
Test double is a generic term for any object that stands in for a real object during a test (think "stunt double"). You create one using the double method.
We will use double because we don't care about any specific implementation of the validator
. At this moment we do care about logic related to adding "processed" string to data.
Now we have our validator
double, and we should allow it to receive valid?
message. Let's stub that method:
allow(validator).to receive(:valid?).and_return(true)
I like RSpec because of this nice DSL. Now our validator
responds to valid?
and returns true
.
There is a way to define double with stubbed methods, instead two lines we can have one:
validator = double(:validator, valid?: true)
Code of our spec looks like this now:
require 'spec_helper'
describe DataProcessor do
let(:processor) { described_class.new }
it 'adds processed to valid data' do
validator = double(:validator, valid?: true)
expect(processor.process('foo', validator)).to eq('foo processed')
end
end
The spec is green.
Validator we passed to process
method responded true
to valid?
call, so our data was processed.
Let's add another case to check if process
method throws an Error
for invalid data.
Still, we're not interested in the validator
, we just want it to return false
for valid?
method.
require 'spec_helper'
describe DataProcessor do
let(:processor) { described_class.new }
context 'with valid data' do
it 'adds processed to data' do
validator = double(:validator, valid?: true)
expect(processor.process('foo', validator)).to eq('foo processed')
end
end
context 'with invalid data' do
it 'raises Error' do
validator = double(:validator, valid?: false)
expect { processor.process('foo', validator) }.to raise_error(DataProcessor::Error)
end
end
end
I added the case that checks if it raises Error
also I've rearranged cases to contexts for better reading.
For that case, we need to stub valid?
method so it returns false
.
validator = double(:validator, valid?: false)
Ok, we know that it raises an exception for invalid data and we know that it adds "processed" to valid data. The last step to make sure that we actually call validator.valid?
method. Now, we have to set up an expectation for validator
to receive valid?
method call.
If we want to make sure that object receives any message during execution, we should use mocks. Let's add one more case to make sure that process
method is calling validator.valid?(data)
during execution.
it 'calls validator.valid?' do
validator = double(:validator)
expect(validator).to receive(:valid?).with('foo').and_return(true)
processor.process('foo', validator)
end
For this case, we created our basic object (double) and then we set an expectation. We expect it to receive valid?
with foo
and return true
.
That's the main difference between mocks and stubs. In case of stubs we allow
object to receive a message, in case of mocks we expect
them to receive it.
If we remove this line from code:
raise Error unless validator.valid?(data)
Our last case will fail, with the following error:
(Double :validator).valid?("foo")
expected: 1 time with arguments: ("foo")
received: 0 times
If that was a stub (allow
) it wouldn't throw an error, but with mock, we expect validator
to receive valid?
at least once.
Stubs and mocks work not just for doubles. We can stub and mock methods for real objects.
Let's say that we don't use dependency injection for validator, instead we have it hard coded inside process
method:
class DataProcessor
Error = Class.new(StandardError)
def process(data)
raise Error unless Validator.new.valid?(data)
"#{data} processed"
end
end
class Validator
def valid?(data)
true
end
end
As we can see from this example, now process
accepts just data
parameter, and we use Validator
to validate it.
How can we change our specs to cover all cases we had? For cases like these, we should consider using dependency injection if it makes sense, and refactor code. If we want to stick to current implementation and have test coverage, we can use methods that RSpec provides for us:
allow_any_instance_of
expect_any_instance_of
We can use those methods to add mocks or stubs to any instance of Validator
. We instantiate an instance of Validator
in process
method, so that's exactly what we need in this case.
require 'spec_helper'
describe DataProcessor do
let(:processor) { described_class.new }
context 'with valid data' do
it 'adds processed to data' do
# it works because true is default value for Validator
expect(processor.process('foo')).to eq('foo processed')
end
end
context 'with invalid data' do
it 'raises Error' do
allow_any_instance_of(Validator).to receive(:valid?).and_return(false)
expect { processor.process('foo') }.to raise_error(DataProcessor::Error)
end
end
it 'calls validator.valid?' do
expect_any_instance_of(Validator).to receive(:valid?).with('foo').and_return(true)
processor.process('foo')
end
end
All specs are green again. Great!
I hope these examples helped to understand the difference between mocks and stubs.
Also, I can recommend this website if you want to learn how to write better specs: betterspecs.org.
Let me know if it was interesting reading for you or you have topics you want me to cover.
Thanks for reading!
-Sergii