RSpec: difference between mocks and stubs

Subscribe to receive new articles. No spam. Quality content.

Follow @makagon

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

Golden Gate from both sides. Amazing place. #sanfrancisco #goldengatebridge #bridge #sanfran A post shared by Sergii Makagon (@s.makagon) on Oct 9, 2017 at 7:23pm PDT

Subscribe to receive new articles. No spam. Quality content.

Follow @makagon