Skip to content

Ruby Advanced Testing — RSpec Mocks, Stubs, Factories, and Integration Testing

DodaTech Updated 2026-06-28 4 min read

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