Skip to content
AgileLogics
6 min read

Test Doubles: Stubs and Mocks

Prerequisites: Testing with Minitest, Testing with RSpec

Some code is hard to test directly because it talks to the outside world: it charges a card, sends an email, or calls a slow API. A test double stands in for that dependency during the test.

  • A stub returns a canned answer: "when asked for the weather, say 21°".
  • A mock also checks it was called correctly: "expect exactly one charge call for 50".

Design for it: inject dependencies

The easiest code to test takes its collaborators as arguments instead of creating them inside:

class Checkout
  def initialize(gateway:)
    @gateway = gateway
  end

  def pay(order_total)
    result = @gateway.charge(amount: order_total)
    result[:ok] ? "paid" : "declined"
  end
end

In production you pass the real payment gateway; in tests, a double. This is called dependency injection.

Doubles in plain Ruby

Thanks to duck typing, a double can be a tiny class:

require "minitest/autorun"

class FakeGateway
  attr_reader :charges

  def initialize(ok:)
    @ok = ok
    @charges = []
  end

  def charge(amount:)
    @charges << amount
    { ok: @ok }
  end
end

class CheckoutTest < Minitest::Test
  def test_successful_payment
    gateway = FakeGateway.new(ok: true)

    assert_equal "paid", Checkout.new(gateway:).pay(50)
    assert_equal [50], gateway.charges
  end

  def test_declined_payment
    assert_equal "declined", Checkout.new(gateway: FakeGateway.new(ok: false)).pay(50)
  end
end

Doubles in RSpec

RSpec has doubles built in. instance_double checks that the stubbed methods really exist on the real class, so tests fail if the real interface changes:

RSpec.describe Checkout do
  it "charges the order total" do
    gateway = instance_double(PaymentGateway)
    allow(gateway).to receive(:charge).and_return({ ok: true })

    expect(Checkout.new(gateway:).pay(50)).to eq("paid")
    expect(gateway).to have_received(:charge).with(amount: 50)
  end
end

Faking HTTP

For code that makes HTTP requests directly, the WebMock gem blocks real network access in tests and lets you stub responses by URL; VCR records real responses once and replays them.

Guidelines

  • Double what you don't own and what's slow or has side effects (payments, email, third-party APIs). Don't double simple objects from your own code — use the real ones.
  • Too many mocks in one test is a sign the code under test has too many dependencies.
  • Keep at least a few tests that use the real integration, so the doubles can't drift away from reality.