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
chargecall 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.