rspec-mocks

bastos's avatarfrom bastos

This skill should be used when the user asks about "test doubles", "mocking", "stubbing", "spies", "verifying doubles", "partial doubles", "allow", "receive", "have_received", or needs guidance on isolating tests and mocking dependencies in RSpec.

0stars🔀0forks📁View on GitHub🕐Updated Jan 10, 2026

When & Why to Use This Skill

This RSpec Mocks skill provides comprehensive guidance and code examples for isolating Ruby code during testing. It covers the implementation of test doubles, stubs, spies, and verifying doubles, helping developers create reliable, decoupled unit tests while ensuring interface consistency through best practices.

Use Cases

  • Isolating code under test from external dependencies like third-party APIs, mailers, or databases using test doubles.
  • Verifying object interactions and behavior using message expectations and spies to ensure specific methods are called with correct arguments.
  • Ensuring test suite maintainability by using verifying doubles (instance_double, class_double) that validate stubs against real class interfaces.
  • Simulating complex edge cases such as network timeouts, database errors, or specific return sequences to test error handling logic.
  • Refactoring legacy codebases by using partial doubles to stub specific methods on real objects without needing to mock the entire system.
nameRSpec Mocks
descriptionThis skill should be used when the user asks about "test doubles", "mocking", "stubbing", "spies", "verifying doubles", "partial doubles", "allow", "receive", "have_received", or needs guidance on isolating tests and mocking dependencies in RSpec.
version1.0.0

RSpec Mocks

RSpec Mocks provides test doubles for isolating code under test from external dependencies.

Test Double Types

Type Purpose
Double Pure test object with no connection to real class
Verifying Double Double that validates against real class interface
Partial Double Real object with some methods stubbed
Spy Records method calls for later verification

Basic Doubles

Creating Doubles

# Anonymous double
user = double

# Named double (better error messages)
user = double("user")

# Double with stubs
user = double("user", name: "John", email: "john@example.com")

Stubbing Methods

user = double("user")
allow(user).to receive(:name).and_return("John")
allow(user).to receive(:save).and_return(true)

# Multiple stubs at once
allow(user).to receive_messages(name: "John", email: "john@example.com")

Return Values

allow(service).to receive(:call).and_return("result")

# Return different values on consecutive calls
allow(service).to receive(:call).and_return(1, 2, 3)
# First call returns 1, second returns 2, third+ returns 3

# Return value from block
allow(service).to receive(:call) { |arg| arg.upcase }

# Raise error
allow(service).to receive(:call).and_raise(StandardError, "error message")
allow(service).to receive(:call).and_raise(CustomError.new("message"))

# Throw symbol
allow(service).to receive(:call).and_throw(:abort)

# Yield to block
allow(service).to receive(:call).and_yield("value")
allow(service).to receive(:call).and_yield(1).and_yield(2)

# Call original implementation (partial doubles)
allow(service).to receive(:call).and_call_original

Verifying Doubles

Verifying doubles check that stubbed methods exist on the real class. Always prefer verifying doubles over plain doubles.

# instance_double - verifies against instance methods
user = instance_double(User)
allow(user).to receive(:name).and_return("John")
allow(user).to receive(:nonexistent)  # Raises error!

# class_double - verifies against class methods
UserService = class_double(UserService)
allow(UserService).to receive(:find).and_return(user)

# object_double - verifies against specific object
original_user = User.new
user = object_double(original_user, name: "John")

Verifying Double Benefits

# If User class changes and removes `name` method:
# - Plain double: Tests pass but code is broken
# - Verifying double: Tests fail, alerting to the issue

user = instance_double(User)
allow(user).to receive(:fullname)  # Typo! Raises:
# User does not implement #fullname

Null Object Doubles

Return nil for unstubbed methods instead of raising:

user = instance_double(User).as_null_object
user.anything  # Returns nil instead of error

Message Expectations

expect vs allow

# allow - Stub without requiring call (test setup)
allow(service).to receive(:call)

# expect - Must be called or test fails (behavior verification)
expect(service).to receive(:call)

Verifying Calls

# Must be called
expect(mailer).to receive(:send_email)

# Must be called with specific arguments
expect(mailer).to receive(:send_email).with("user@example.com", "Welcome!")

# Must be called specific number of times
expect(mailer).to receive(:send_email).once
expect(mailer).to receive(:send_email).twice
expect(mailer).to receive(:send_email).exactly(3).times
expect(mailer).to receive(:send_email).at_least(:once)
expect(mailer).to receive(:send_email).at_most(5).times

# Must not be called
expect(mailer).not_to receive(:send_spam)

Argument Matchers

expect(service).to receive(:call).with("exact value")
expect(service).to receive(:call).with(anything)
expect(service).to receive(:call).with(any_args)
expect(service).to receive(:call).with(no_args)

# Type matching
expect(service).to receive(:call).with(instance_of(User))
expect(service).to receive(:call).with(kind_of(Numeric))

# Pattern matching
expect(service).to receive(:call).with(/pattern/)
expect(service).to receive(:call).with(hash_including(key: "value"))
expect(service).to receive(:call).with(array_including(1, 2))

# Custom matching
expect(service).to receive(:call).with(satisfy { |arg| arg.valid? })

# Combining matchers
expect(service).to receive(:process).with(
  instance_of(User),
  hash_including(notify: true)
)

Spies

Spies verify calls after they happen (more natural test flow):

# Setup: allow the call
mailer = instance_double(Mailer)
allow(mailer).to receive(:send_email)

# Exercise: run the code
user_service = UserService.new(mailer)
user_service.register(user)

# Verify: check it was called
expect(mailer).to have_received(:send_email).with(user.email)

Spy vs Mock Style

# Mock style (expect before action)
expect(mailer).to receive(:send_email)
user_service.register(user)

# Spy style (verify after action) - often clearer
allow(mailer).to receive(:send_email)
user_service.register(user)
expect(mailer).to have_received(:send_email)

Spy Helpers

# Create a spy that tracks all calls
user = spy("user")
user.name
user.email
user.save

expect(user).to have_received(:name)
expect(user).to have_received(:save)

# Verifying spy
user = instance_spy(User)

Partial Doubles

Stub methods on real objects:

user = User.new(name: "John")
allow(user).to receive(:premium?).and_return(true)

user.name       # Returns "John" (real method)
user.premium?   # Returns true (stubbed)

Class Method Stubbing

allow(User).to receive(:find).and_return(user)
allow(Time).to receive(:now).and_return(frozen_time)
allow(ENV).to receive(:[]).with("API_KEY").and_return("test-key")

Dangerous: Stubbing Any Instance

# Avoid when possible - makes tests brittle
allow_any_instance_of(User).to receive(:premium?).and_return(true)
expect_any_instance_of(User).to receive(:save)

Better alternative: Dependency injection

# Instead of stubbing any instance
class UserService
  def initialize(user_class: User)
    @user_class = user_class
  end

  def create(attrs)
    @user_class.new(attrs)
  end
end

# Test with injected double
user_class = class_double(User)
service = UserService.new(user_class: user_class)

Ordering

Enforce call order:

expect(logger).to receive(:start).ordered
expect(processor).to receive(:process).ordered
expect(logger).to receive(:finish).ordered

Configuration

# spec/spec_helper.rb
RSpec.configure do |config|
  config.mock_with :rspec do |mocks|
    # Verify partial doubles against real methods
    mocks.verify_partial_doubles = true

    # Verify doubles in before/after hooks
    mocks.verify_doubled_constant_names = true
  end
end

Best Practices

Use Verifying Doubles

# Good - catches interface changes
user = instance_double(User, name: "John")

# Avoid - doesn't verify interface
user = double("user", name: "John")

Prefer Spies for Verification

# Good - arrange, act, assert order
allow(mailer).to receive(:send)
service.process
expect(mailer).to have_received(:send)

# Harder to read - expect before action
expect(mailer).to receive(:send)
service.process

Don't Over-Mock

# Too much mocking - testing implementation
allow(user).to receive(:first_name).and_return("John")
allow(user).to receive(:last_name).and_return("Doe")
expect(user.full_name).to eq("John Doe")  # Just testing string concat

# Better - test real behavior
user = build(:user, first_name: "John", last_name: "Doe")
expect(user.full_name).to eq("John Doe")

Mock at Boundaries

Mock external services, not internal collaborators:

# Good - mocking external HTTP
allow(HTTPClient).to receive(:get).and_return(response)

# Questionable - mocking internal service
allow(UserValidator).to receive(:validate)  # Maybe just use real one?

Additional Resources

Reference Files

  • references/mock-patterns.md - Common mocking patterns
  • references/test-isolation.md - When and what to mock

Example Files

  • examples/service_with_mocks.rb - Service testing with mocks
  • examples/api_client_spec.rb - Mocking HTTP clients
rspec-mocks – AI Agent Skills | Claude Skills