| name | rspec-mocks |
| description | 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. |
| version | 1.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
user = double
user = double("user")
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)
allow(user).to receive_messages(name: "John", email: "john@example.com")
Return Values
allow(service).to receive(:call).and_return("result")
allow(service).to receive(:call).and_return(1, 2, 3)
allow(service).to receive(:call) { |arg| arg.upcase }
allow(service).to receive(:call).and_raise(StandardError, "error message")
allow(service).to receive(:call).and_raise(CustomError.new("message"))
allow(service).to receive(:call).and_throw(:abort)
allow(service).to receive(:call).and_yield("value")
allow(service).to receive(:call).and_yield(1).and_yield(2)
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.
user = instance_double(User)
allow(user).to receive(:name).and_return("John")
allow(user).to receive(:nonexistent)
UserService = class_double(UserService)
allow(UserService).to receive(:find).and_return(user)
original_user = User.new
user = object_double(original_user, name: "John")
Verifying Double Benefits
user = instance_double(User)
allow(user).to receive(:fullname)
Null Object Doubles
Return nil for unstubbed methods instead of raising:
user = instance_double(User).as_null_object
user.anything
Message Expectations
expect vs allow
allow(service).to receive(:call)
expect(service).to receive(:call)
Verifying Calls
expect(mailer).to receive(:send_email)
expect(mailer).to receive(:send_email).with("user@example.com", "Welcome!")
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
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)
expect(service).to receive(:call).with(instance_of(User))
expect(service).to receive(:call).with(kind_of(Numeric))
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))
expect(service).to receive(:call).with(satisfy { |arg| arg.valid? })
expect(service).to receive(:process).with(
instance_of(User),
hash_including(notify: true)
)
Spies
Spies verify calls after they happen (more natural test flow):
mailer = instance_double(Mailer)
allow(mailer).to receive(:send_email)
user_service = UserService.new(mailer)
user_service.register(user)
expect(mailer).to have_received(:send_email).with(user.email)
Spy vs Mock Style
expect(mailer).to receive(:send_email)
user_service.register(user)
allow(mailer).to receive(:send_email)
user_service.register(user)
expect(mailer).to have_received(:send_email)
Spy Helpers
user = spy("user")
user.name
user.email
user.save
expect(user).to have_received(:name)
expect(user).to have_received(:save)
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
user.premium?
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
allow_any_instance_of(User).to receive(:premium?).and_return(true)
expect_any_instance_of(User).to receive(:save)
Better alternative: Dependency injection
class UserService
def initialize(user_class: User)
@user_class = user_class
end
def create(attrs)
@user_class.new(attrs)
end
end
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
RSpec.configure do |config|
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
mocks.verify_doubled_constant_names = true
end
end
Best Practices
Use Verifying Doubles
user = instance_double(User, name: "John")
user = double("user", name: "John")
Prefer Spies for Verification
allow(mailer).to receive(:send)
service.process
expect(mailer).to have_received(:send)
expect(mailer).to receive(:send)
service.process
Don't Over-Mock
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")
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:
allow(HTTPClient).to receive(:get).and_return(response)
allow(UserValidator).to receive(:validate)
Additional Resources
references/mock-patterns.md - Common mocking patterns