DEV Community

Vasily Polovnyov
Vasily Polovnyov

Posted on • Updated on

Explaining RSpec doubles, mocks and stubs in 2 minutes

This post is for those who are familiar with RSpec, but do not understand the meaning of "to mock something" or "to stub something". Short, to the point, and with examples.

Double

A stunt object that substitutes a real system object during tests:

describe NotificationsController do
  # The NotificationsController downloads
  # the latest notifications from a third-party service
  # via HTTP call using NotificationsDatasource
  let(:datasource) do
    double(:datasource, as_json: { notifications: [] })
  end

  before do
    # Replace the real NotificationsDatasource with a double,
    # avoiding external depency in the test:
    allow(NotificationsDatasource)
      .to receive(:new)
      .and_return(datasource)
  end

  describe "#index" do
    it "wraps notifications in 'data' key" do
      get :index, format: :json

      expect(json_response["data"].keys)
        .to have_key "notifications"
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Stub

A stub for a method or object that returns canned value:

context "when attachment file is too large to email" do
  let(:max_file_size) { Attachment::MAX_FILE_SIZE }

  before do
    allow(attachment)
      .to receive(:file_size)
      .and_return(max_file_size + 1)
  end

  it "raises 'file is too large' error" do
    # ...
  end
end
Enter fullscreen mode Exit fullscreen mode

The attentive reader has already noticed that in the previous example with NotificationsController there was a stub, too. That's right: a stub is a double with pre-defined responses.

Mock

Stub with expectations, which RSpec will check at the end of the test:

context "when cloning process cannot be performed" do
  before do
    allow(doctor).to receive(:clone) { raise "can't clone" } # stub
  end

  it "notifies airbrake" do
    expect(Airbrake).to receive(:notify) # mock
    # RSpec will stub `Airbrake.notify`
    # and at the end of this `it do...end` block
    # will check, if it has been called.
    # If it hasn't been called,
    # it would raise an error and fail the test.
    #
    # So when asked at a job interview what's
    # the difference between a mock and a stub, answer,
    # "Only a mock can fail a test."

    clone_poor_dolly
  end
end
Enter fullscreen mode Exit fullscreen mode

Mocks change the order of phases in the test. Instead of "Setup — Exercise — Verify" you get "Setup & Verify — Exercise". If you, like me, find this hard to read, use a stub with a check:

# mock
it "notifies airbrake" do
  expect(Airbrake).to receive(:notify) # setup & verify

  clone_poor_dolly # exercise
end

# stub
it "notifies airbrake" do
  allow(Airbrake).to receive(:notify) # setup

  clone_poor_dolly # exercise

  expect(Airbrake).to have_received(:notify) # verify
end
Enter fullscreen mode Exit fullscreen mode

More on this topic:

Top comments (0)