Ruby Advanced Testing — RSpec Mocks, Stubs, Factories, and Integration Testing
In this tutorial, you will learn about Ruby Advanced Testing. We cover key concepts, practical examples, and best practices to help you master this topic.
Ruby advanced testing covers RSpec mocks and stubs for isolation, FactoryBot for test data, Capybara for integration testing, and VCR for external API calls.
What You'll Learn
- RSpec mocks, stubs, and expectations
- FactoryBot for test data creation
- Capybara for feature testing
- VCR for HTTP recording/replay
Why It Matters
Advanced testing ensures code quality and prevents regressions. GitHub runs millions of tests daily. Shopify has extensive test suites. DodaZIP uses VCR to test API integrations.
Real-World Use
Continuous integration pipelines, regression prevention, Test-Driven Development, API client testing, browser automation.
flowchart LR
A["Advanced Testing"] --> B["Mocks/Stubs"]
A --> C["FactoryBot"]
A --> D["Capybara"]
A --> E["VCR"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Mocks and Stubs
RSpec.describe OrderProcessor do
let(:payment_gateway) { instance_double("PaymentGateway") }
let(:processor) { OrderProcessor.new(payment_gateway) }
before do
allow(payment_gateway).to receive(:charge)
.with(5000, "USD")
.and_return(true)
end
it "charges the payment gateway" do
processor.process_order(amount: 5000)
expect(payment_gateway).to have_received(:charge).with(5000, "USD")
end
it "handles payment failure" do
allow(payment_gateway).to receive(:charge).and_return(false)
result = processor.process_order(amount: 5000)
expect(result).to be_falsey
end
end
FactoryBot
# spec/factories/users.rb
FactoryBot.define do
factory :user do
name { "Alice" }
email { "alice@example.com" }
admin { false }
trait :admin do
admin { true }
end
trait :with_posts do
after(:create) do |user|
create_list(:post, 3, user: user)
end
end
end
end
# spec/models/user_spec.rb
RSpec.describe User do
it "creates a regular user" do
user = create(:user)
expect(user.admin).to be false
end
it "creates an admin" do
admin = create(:user, :admin)
expect(admin.admin).to be true
end
end
Capybara Feature Tests
# spec/features/tasks_spec.rb
require "rails_helper"
RSpec.feature "Task management" do
scenario "User creates a new task" do
visit "/tasks"
click_on "New Task"
fill_in "Title", with: "Buy groceries"
click_on "Create Task"
expect(page).to have_content("Task was created")
expect(page).to have_content("Buy groceries")
end
scenario "User completes a task" do
task = create(:task, title: "Learn Ruby")
visit "/tasks"
check "task_#{task.id}"
expect(page).to have_content("Task completed")
end
end
VCR for HTTP Tests
# spec/spec_helper.rb
require "vcr"
VCR.configure do |config|
config.cassette_library_dir = "spec/cassettes"
config.hook_into :webmock
config.default_cassette_options = { record: :new_episodes }
end
# spec/services/weather_service_spec.rb
RSpec.describe WeatherService do
it "fetches weather data" do
VCR.use_cassette("weather/san_francisco") do
service = WeatherService.new
result = service.forecast(lat: 37.7749, lon: -122.4194)
expect(result[:temperature]).to be_present
end
end
end
Shared Examples
RSpec.shared_examples "a serializable model" do
it "serializes to JSON" do
expect(subject.as_json).to include(:id, :created_at, :updated_at)
end
end
RSpec.describe User, type: :model do
subject { create(:user) }
it_behaves_like "a serializable model"
end
RSpec.describe Post, type: :model do
subject { create(:post) }
it_behaves_like "a serializable model"
end
Common Mistakes
1. Over-mocking
Don't mock everything. Test real behavior where possible. Mock external services and side effects only.
2. Leaky Factory Data
Use let or let! for test data. Clean database between tests with database_cleaner or transactional tests.
3. Slow Feature Tests
Capybara tests are slow. Test critical user flows only. Use request specs for API testing instead.
4. Stale VCR Cassettes
Delete and re-record cassettes when APIs change. Use record: :new_episodes for gradual updates.
5. Testing Implementation Details
Test behavior, not implementation. Don't test private methods. Don't assert on internal state.
Practice Questions
1. What's the difference between a mock and a stub? A stub returns a value. A mock sets an expectation that a method was called. Use allow for stubs, expect for mocks.
2. How does FactoryBot differ from fixtures? Factories define reusable patterns. They support traits, associations, and callbacks. Fixtures are static YAML files.
3. What is VCR used for? Records HTTP interactions as cassettes. Replays them in tests. Fast and deterministic external API testing.
4. What's the purpose of Capybara? Simulates user interactions with the browser. Useful for integration testing full user flows.
Challenge: Write a VCR test for a class that calls the GitHub API to fetch user repos.
Solution
# spec/services/github_client_spec.rb
RSpec.describe GithubClient do
it "fetches user repositories" do
VCR.use_cassette("github/user_repos") do
client = GithubClient.new
repos = client.repos("octocat")
expect(repos).to all(have_key(:name))
expect(repos).to all(have_key(:language))
end
end
end
FAQ
{{< faq question="Should I use shoulda-matchers?" >}}
Yes. They provide one-liner tests for validations, associations, and callbacks. it { should validate_presence_of(:name) }.
{{< /faq >}}
{{< faq question="What's the difference between let and let!?" >}}
let is lazily evaluated (evaluated when first referenced). let! is eagerly evaluated before each example. Use let! when you need the record before the test runs.
{{< /faq >}}
{{< faq question="How do I test file uploads?" >}}
Use fixture_file_upload in Rails tests or Rack::Test::UploadedFile. Capybara has attach_file for feature tests.
{{< /faq >}}
{{< faq question="What is database_cleaner?" >}} A gem that cleans the database between tests. Configurable strategies: Transaction, truncation, and deletion. Use transactions for most tests, truncation for feature tests. {{< /faq >}}
{{< faq question="How do I test background jobs?" >}}
Use perform_enqueued_jobs block in tests. Test job behavior directly. Use have_enqueued_job matcher for queue assertions.
{{< /faq >}}
Try It Yourself
require "rspec/autorun"
class Calculator
def add(a, b) = a + b
end
RSpec.describe Calculator do
subject(:calc) { Calculator.new }
it "adds numbers" do
expect(calc.add(2, 3)).to eq(5)
end
it "works with negative numbers" do
expect(calc.add(-1, 1)).to eq(0)
end
end
Expected output — 2 examples, 0 failures.
What's Next
Now that you understand advanced testing, explore deployment strategies for Ruby applications.
| Topic | Description | Link |
|---|---|---|
| Ruby Deployment | Deploying Ruby applications | {{< ref "48-deployment" >}} |
| Ruby Testing | Basics of Ruby testing | {{< ref "32-testing" >}} |
| Go Testing | Compare Go testing | Go |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro