Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
56: Mocking and Stubbing in RSpec
I've seen this happen a dozen times with junior devs: you're building a feature that integrates with a third-party API—maybe Stripe for payments or Twilio for SMS—and you write a test that looks perfect. But the moment you run your test suite in CI, it fails. Why? Because the CI server doesn't have your local API keys, or the third-party sandbox is down, or you've hit your rate limit for the hour.
# The code we're testing
class OrderProcessor
def initialize(payment_gateway = StripeGateway.new)
@payment_gateway = payment_gateway
end
def process(order)
response = @payment_gateway.charge(order.amount, order.currency)
if response.success?
order.mark_as_paid!
return true
end
false
end
end
# The "broken" test
RSpec.describe OrderProcessor do
it "marks the order as paid when the charge is successful" do
order = Order.new(amount: 100, currency: 'usd')
processor = OrderProcessor.new
# This actually hits the Stripe API!
expect(processor.process(order)).to be true
expect(order.paid?).to be true
end
end
The Danger of Real API Calls in Tests
The test above is what I call a "brittle test." It's not actually testing your OrderProcessor logic; it's testing the network connection, the Stripe API's uptime, and your API credentials. If any of those fail, your test fails, even if your Ruby code is flawless. Plus, imagine if this were a production API—you'd be charging your own credit card every time you ran your tests. That's a quick way to go broke while trying to reach 100% test coverage.
Stubbing the Response for Predictability
To fix this, we use Stubbing. Stubbing is essentially telling RSpec: "When this specific method is called on this specific object, don't actually run the code inside that method. Instead, just return this value I'm giving you."
Here is how we isolate the OrderProcessor from the rest of the world:
RSpec.describe OrderProcessor do
it "marks the order as paid when the charge is successful" do
order = Order.new(amount: 100, currency: 'usd')
# 1. Create a double (a fake object) for the gateway
gateway = instance_double("StripeGateway")
# 2. Stub the charge method to return a fake success response
# We create a fake response object that responds to .success?
success_response = double("Response", success?: true)
allow(gateway).to receive(:charge).and_return(success_response)
processor = OrderProcessor.new(gateway)
expect(processor.process(order)).to be true
expect(order.paid?).to be true
end
end
By using instance_double, we've created a "test double." I prefer instance_double over a generic double because it verifies that the method we are stubbing (:charge) actually exists on the real StripeGateway class. If you rename the method in your actual code but forget to update the test, RSpec will throw an error. It prevents your mocks from becoming outdated.
Using Mocks to Ensure the Call Actually Happened
Stubbing is about providing input to your system. But sometimes, you need to verify output—not just the return value, but that a specific action was taken. This is where Mocking comes in. While stubbing says "If this is called, return X," mocking says "I expect this to be called exactly like this, or the test fails."
Let's say we want to make sure we are passing the correct amount to the gateway. We change allow to expect:
it "charges the correct amount to the gateway" do
order = Order.new(amount: 50, currency: 'usd')
gateway = instance_double("StripeGateway")
success_response = double("Response", success?: true)
# This is a mock. If .charge is NOT called with these exact args, the test fails.
expect(gateway).to receive(:charge).with(50, 'usd').and_return(success_response)
processor = OrderProcessor.new(gateway)
processor.process(order)
end
A quick word of advice: don't over-mock. If you mock every single method call in your app, you're no longer testing your software; you're just testing that your code calls other code. Use stubs for external boundaries (APIs, Databases, File Systems) and mocks when the fact that a call happened is the primary goal of the test.
📋 Practical Task
Exercise: Mocking the Weather Alert System
You are building a WeatherNotifier class. It depends on a WeatherClient class that fetches the current temperature from a remote API. If the temperature is below 32 degrees, the notifier should send an alert via a SmsService.
Your Task: Write a test for the WeatherNotifier#check_and_notify method. You must:
- Use
instance_doubleto mock theWeatherClientso it returns a temperature of 25 degrees. - Use a mock to verify that the
SmsServicereceives the method:send_alertwith the message "Freeze warning!" exactly once. - Ensure that if the
WeatherClientreturns 75 degrees, theSmsServiceis not called.
Starter Code:
class WeatherNotifier
def initialize(client, sms_service)
@client = client
@sms_service = sms_service
end
def check_and_notify(city)
temp = @client.get_temperature(city)
if temp < 32
@sms_service.send_alert("Freeze warning!")
end
end
end
# You need to write the RSpec tests for this class.
# Assume WeatherClient has a method #get_temperature(city)
# Assume SmsService has a method #send_alert(message)
There are no comments for now.