Chapter III - Mocking and Stubbing
(avr. time for this chapter: 1 to 2 days)
When testing, you often need to isolate the code under test from its dependencies. Mocking and stubbing allow you to replace real objects with test doubles, making your tests faster, more reliable, and focused on the specific behavior you're testing.
In this chapter, you will learn how to use RSpec's built-in mocking framework to create test doubles, stubs, and mocks—particularly useful for testing your ebook purchase flow and email notifications.
What are Test Doubles?
Test doubles are objects that stand in for real objects in your tests. They come in several flavors:
- Dummy - objects passed around but never used
- Stub - provides canned answers to calls made during the test
- Mock - objects pre-programmed with expectations
- Spy - records information about how it was called
- Fake - working implementations with shortcuts
Reference: RSpec Mocks Documentation
Creating Doubles
Steps to implement:
- Create a simple double:
double("ebook") - Create a double with methods:
user_double = double("user", name: "John", email: "john@example.com") - Use
instance_doublefor verified doubles (recommended):ebook_double = instance_double(Ebook, title: "Ruby Guide", price: 19.99) - Use
class_doublefor stubbing class methods:ebook_class = class_double(Ebook) allow(ebook_class).to receive(:published).and_return([ebook_double])
Stubbing Methods
Stubbing replaces method implementations with predetermined responses.
Steps to implement:
- Stub a method on a double:
allow(ebook).to receive(:price).and_return(29.99) - Stub a method on a real object (useful for User.find in controllers):
allow(User).to receive(:find).and_return(user_double) allow(Ebook).to receive(:published).and_return([ebook1, ebook2]) - Stub with different return values for consecutive calls:
allow(ebook).to receive(:view_count).and_return(10, 11, 12) - Stub to raise an exception:
allow(ebook).to receive(:publish!).and_raise(InvalidStatusTransition) - Stub with block for dynamic responses:
allow(Ebook).to receive(:by_seller) { |user| ebooks.select { |e| e.seller == user } }
Mocking with Expectations
Mocks verify that methods are called as expected—essential for testing your email notifications.
Steps to implement:
- Set expectation that the mailer is called when purchasing an ebook:
expect(PurchaseMailer).to receive(:seller_notification) - Verify method is called with specific arguments:
expect(PurchaseMailer).to receive(:seller_notification).with(seller, ebook, purchase) - Verify method is called a specific number of times:
expect(StatisticsTracker).to receive(:record_view).exactly(3).times - Use argument matchers:
expect(PurchaseMailer).to receive(:buyer_confirmation).with(anything, hash_including(ebook_id: ebook.id))
Using Spies
Spies allow you to verify calls after the fact, which can make tests more readable.
Steps to implement:
- Create a spy for your mailer:
mailer_spy = spy("PurchaseMailer") - Perform the action in your test
- Verify calls were made:
expect(mailer_spy).to have_received(:seller_notification).with(seller, ebook)
Mocking External Services
Your ebook application sends emails and tracks statistics. Mock these to avoid side effects in tests.
Steps to implement:
- Stub mailer deliveries:
allow(PurchaseMailer).to receive(:seller_notification).and_return(double(deliver_later: true)) allow(PurchaseMailer).to receive(:buyer_confirmation).and_return(double(deliver_later: true)) - Mock statistics tracking service:
allow(StatisticsService).to receive(:track_view) allow(StatisticsService).to receive(:track_download) - Use WebMock for external HTTP requests (if you have external APIs):
stub_request(:post, "https://analytics.example.com/events") .to_return(status: 200, body: '{"success": true}')
Reference: WebMock
Best Practices
- Mock what you don't own - mock email services, external APIs, not your own models
- Don't mock the object under test - test real behavior of Ebook, User, Purchase
- Use verified doubles -
instance_double(Ebook)catches method name typos - Prefer stubs over mocks - only mock when you need to verify calls
- Keep mocks simple - complex mock setups indicate design problems
Exercise
Apply these concepts to your ebook application:
- Mock the email service in purchase tests:
- Stub
PurchaseMailer.seller_notificationto avoid sending real emails - Verify that the mailer is called with correct seller and ebook
-
Verify that
buyer_confirmationis called with purchase details -
Mock the statistics tracking:
- Stub
Ebook#record_viewwhen testing the show action - Verify view count is recorded with correct data (IP, browser, etc.)
-
Stub PDF download tracking
-
Stub complex queries in controller tests:
- Stub
Ebook.publishedto return predictable data - Stub
User.findto return a test user -
Use
instance_doublefor ebook instances -
Test error handling:
- Stub
ebook.purchase!to raise an exception - Verify your controller handles the error gracefully
- Test what happens when email delivery fails