DEV Community

loading...

The difference between Mocks, Stubs, and Spies

Hideaki Ishii
I'm a Software Engineer, who mainly uses Ruby / Rails, JavaScript / TypeScript, Golang, etc.
・3 min read

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) { 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.

spy(SomeLib) is similar to instance_double(SomeLib). The difference is:

  • spy
    • Mix stubs and real objects.
    • If some real method is not stubbed, the real one would be called.
  • instance_double
    • Can stub methods
    • If you try to stub the undefined method, it would raise an error.

Summary

Let's clarify the difference and write excellent tests!

Reference

Discussion (0)