Skip to content

RSpec: Mocking vs Stubbing

RSpec: Mocking vs Stubbing

Most people learn RSpec by copying a line that works and moving on. That is how you end up with a test suite full of double calls where nobody remembers which of them actually assert something. Mocking and stubbing look almost identical in the editor, but they do opposite jobs: one replaces a value, the other sets an expectation. Get that distinction wrong and your green suite starts lying to you.

A stub answers a question. A mock checks that you asked it.

🧪 The Core Difference

Both mocking and stubbing are forms of test doubles, fake objects that stand in for the real thing so a unit test does not have to hit a database, a network, or a payment provider. The difference is what happens at the end of the test:

  • Stubbing controls what a collaborator returns. Nothing is verified. If the code never calls the stubbed method, the test still passes.
  • Mocking sets an expectation. If the call never happens, or happens with the wrong arguments, the test fails.

In RSpec both are built from the same machinery. allow creates a stub, expect(...).to receive creates a mock. That single word, allow versus expect, is the whole difference.

🎯 Stubbing: Replace the Return Value

Use a stub when the test does not care whether the method is called, only that the rest of the code can keep moving when it is. A typical stub says "when someone asks for this, hand back that."

allow(payment_gateway).to receive(:charge).and_return(true)
allow(clock).to receive(:now).and_return(Time.utc(2026, 4, 4))

Neither line asserts anything. If you deleted the call to charge from your production code, both specs would still pass. That is the point and the danger, all at once.

Stubbing in a real spec

RSpec.describe ReportBuilder do
  it "uses the configured timezone" do
    allow(Time).to receive(:now).and_return(Time.utc(2026, 4, 4, 9, 0))

    report = ReportBuilder.new.build

    expect(report.generated_on).to eq(Date.new(2026, 4, 4))
  end
end

Here the stub is setup, not an assertion. The assertion is the eq on the last line.

✅ Mocking: Set an Expectation

Use a mock when the behavior under test is the call itself. Sending a receipt, publishing to a queue, charging a card: these are side effects you want to prove happened exactly once.

expect(mailer).to receive(:send_receipt).with(42).once

This one does assert. If send_receipt is never called, or is called with the wrong order id, or is called twice, the spec fails with a clear message. That is why mocks belong around interactions with the outside world.

Mixing both in one spec

RSpec.describe OrderCheckout do
  let(:payment_gateway) { instance_double("PaymentGateway") }
  let(:mailer) { instance_double("Mailer") }
  let(:order) { double("Order", id: 42, total: 4999) }

  it "charges the customer and emails a receipt" do
    allow(payment_gateway).to receive(:charge).and_return(true)
    expect(mailer).to receive(:send_receipt).with(42)

    OrderCheckout.new(payment_gateway, mailer).call(order)
  end
end

The gateway is stubbed because the test only needs it to stop throwing. The mailer is mocked because delivering the receipt is the behavior being checked. Same object type, deliberately different treatment.

⚖️ Side by Side

Aspect Stubbing Mocking
RSpec syntax allow(x).to receive(:m) expect(x).to receive(:m)
Purpose Control a return value Verify an interaction
Asserts anything? No Yes
Fails if the call never happens? No Yes
Test smells like Setup Assertion
Reach for it when The collaborator is noise The call is the point

🧭 When to Use Which

A simple rule holds up in practice: stub everything you do not want to think about, mock only the one thing the test is about.

  • Stub expensive or slow collaborators, such as HTTP clients, clocks, and file systems.
  • Stub random values so a test is deterministic.
  • Mock side effects you must guarantee: emails, jobs, payments, cache writes.
  • Keep mocks to one or two per spec. A test with six expectations is testing six things.

⚠️ Common Mistakes

Most brittle suites come from a few repeated slips.

  • Mocking your own classes. Expectations on internal collaborators break the moment you refactor. Mock the boundary, not your own objects.
  • Using a plain double for a real interface. Prefer instance_double and class_double. They fail when the method name or signature does not actually exist, which catches typos that a plain double happily accepts.
  • Stubbing a method that does not exist. By default RSpec raises, so treat that error as a bug in the spec, not an obstacle to silence with verify_partial_doubles = false.
  • Asserting implementation details. If a mock asserts the exact order of three internal calls, every refactor will rewrite the test.

💡 TL;DR

Stubbing is for controlling inputs, mocking is for verifying outputs. Use allow when you just need a fake value back, and expect(...).to receive when the interaction itself is what you are testing. When in doubt, stub more and mock less.

🧩 Conclusion

Mocking and stubbing are not competing techniques, they are two tools with distinct jobs. A stub keeps the rest of your code out of the way so a unit can be tested in isolation. A mock makes a promise about how that unit talks to the world. Draw the line at the boundary of your system, verify the interactions that matter, and let everything else be a quiet, boring stub.

Comments

Popular posts from this blog

How do I permanently remove Youtube From my Android device

How do I make my android phone distraction Free without rooting my device In 2026