This post describes the difference between Mocks, Stubs, and Spies, and I use RSpec to show examples.
I hope this helps you write excellent tests!
TL;DR
In this great article, Mr. Martin Fowler says:
Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.
Spies are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent.
Mocks are what we are talking about here: objects pre-programmed with expectations which form a specification of the calls they are expected to receive.
See the difference with RSpec
Stubs
For example, if some method depends on a third-party API, it's not good to request it whenever testing.
In that case, stubs are very useful.
class SomeClient
def request
body = some_lib.request
JSON.parse body
end
private
def some_lib
@some_lib ||= SomeLib.new
end
end
describe 'SomeClient#request'
before do
allow(client).to receive(:some_lib).with(no_args).and_return(some_lib)
allow(some_lib).to receive(:request).with(no_args).and_return(expected_boby)
end
let(:client) { SomeClient.new }
let(:some_lib) { instance_double(SomeLib) }
let(:expected_body) { ... }
let(:expected_json) { JSON.parse expected_body }
subject { client.request }
it { is_expected.to eq expected_json }
end
We can avoid calling the API actually by stubs.
Mocks
In the above stub example, if the client actually calls the API many times, it's completely unexpected.
If we want to ensure whether the some_lib
method is invoked expectedly, using mocks is more suitable.
describe 'SomeClient#request'
before { allow(client).to receive(:some_lib).with(no_args).and_return(some_lib) }
let(:client) { SomeClient.new }
let(:some_lib) { instance_double(SomeLib) }
let(:expected_body) { ... }
let(:expected_json) { JSON.parse expected_body }
subject { client.request }
it do
expect(some_lib).to receive(:request).with(no_args).and_return(expected_boby).once # Ensure it's really called expectedly.
is_expected.to eq expected_json
end
If SomeLib#request
is called unexpectedly (wrong args, called many times, etc.), the test will fail.
Spies
In rspec-mocks
, though it's a bit difficult to clarify the difference between mocks and spies...
RSpec documentation says:
Message expectations put an example's expectation at the start, before you've invoked the code-under-test. Many developers prefer using an arrange-act-assert (or given-when-then) pattern for structuring tests. Spies are an alternate type of test double that support this pattern by allowing you to expect that a message has been received after the fact, using have_received.
The above example would become with spies like:
describe 'SomeClient#request'
before { allow(client).to receive(:some_lib).with(no_args).and_return(some_lib) }
let(:client) { SomeClient.new }
let(:some_lib) { instance_spy(SomeLib) } # It's OK to use `instance_double` instead, I describe the difference afterward.
let(:expected_body) { ... }
let(:expected_json) { JSON.parse expected_body }
subject { client.request }
it do
is_expected.to eq expected_json
expect(some_lib).to have_received(:request).with(no_args).and_return(expected_boby).once
end
end
It might be more intuitive.
After subject
is called, the test checks whether SomeLib#request
is really called expectedly.
instance_spy(SomeLib)
is similar to instance_double(SomeLib)
. The difference is:
-
instance_double
- Can stub methods.
- If you try to stub the undefined method, it would raise an error.
-
instance_spy
- Mix stubs and real objects.
- If some real method is not stubbed, the real one would be called.
Summary
Let's clarify the difference and write excellent tests!
Top comments (0)