Integration Testing in Ruby

· 4 min read · Updated March 16, 2026 · intermediate
integration-testing rspec capybara testing rails ruby

Integration tests verify that multiple components of your application work together correctly. Unlike unit tests that isolate individual methods, integration tests exercise entire workflows—from controller actions to database changes to rendered views.

Why Integration Tests Matter

Unit tests give you confidence that individual pieces work. Integration tests prove they fit together. A perfectly tested model can still fail when it meets a controller that does not handle its errors correctly.

Integration tests catch problems that unit tests miss:

  • Incorrect routing or URL generation
  • Missing database transactions
  • View rendering issues
  • Session and cookie handling
  • Authentication and authorization flows
  • JSON API response structure
  • Email delivery and content

The tradeoff is speed—integration tests are slower than unit tests because they exercise more of the stack. Balance both in your test suite. Use unit tests for logic and edge cases, integration tests for workflows.

Integration Testing with RSpec

RSpec Rails provides integration testing through feature specs. These simulate browser interactions:

# spec/features/user_registration_spec.rb
require 'rails_helper'

RSpec.describe 'User Registration', type: :feature do
  scenario 'user successfully registers' do
    visit new_user_registration_path
    
    fill_in 'Email', with: 'new@example.com'
    fill_in 'Password', with: 'secure123'
    fill_in 'Password confirmation', with: 'secure123'
    click_button 'Sign up'
    
    expect(page).to have_content('Welcome! You have signed up successfully.')
    expect(User.last.email).to eq('new@example.com')
  end
  
  scenario 'registration fails with invalid email' do
    visit new_user_registration_path
    
    fill_in 'Email', with: 'invalid-email'
    fill_in 'Password', with: 'secure123'
    fill_in 'Password confirmation', with: 'secure123'
    click_button 'Sign up'
    
    expect(page).to have_content('Email is invalid')
    expect(User.count).to eq(0)
  end
end

The type: :feature tag tells RSpec to use Capybara for browser simulation. Each scenario runs in a transaction that is rolled back after the test, keeping the database clean.

Setting Up Capybara

Capybara drives browsers for feature tests. Install it alongside RSpec:

# Gemfile
group :test do
  gem 'capybara'
  gem 'selenium-webdriver'
  gem 'webdrivers'
end

Configure it in your Rails helper:

# spec/rails_helper.rb
require 'capybara/rails'
require 'capybara/rspec'

Capybara.default_driver = :selenium_chrome_headless
Capybara.default_max_wait_time = 5

The headless Chrome driver runs tests without opening a visible browser—essential for CI pipelines. For JavaScript-heavy features, switch to Capybara.javascript_driver = :selenium_chrome.

Testing Database Interactions

Integration tests exercise the full database layer. Use the have_css matcher to verify data persisted:

RSpec.describe 'Creating a Post', type: :feature do
  let(:user) { User.create!(email: 'author@example.com', password: 'password') }
  
  scenario 'user creates and sees their post' do
    login_as(user)
    visit new_post_path
    
    fill_in 'Title', with: 'My First Post'
    fill_in 'Body', with: 'This is the content.'
    click_button 'Create Post'
    
    expect(page).to have_css('.post-title', text: 'My First Post')
    expect(Post.last.title).to eq('My First Post')
  end
end

The login_as helper (from devise or clearance) signs in a user for the test. Without it, authentication would block the request.

Testing JSON APIs

Modern Rails apps often expose JSON APIs. Test them with json: true:

RSpec.describe 'API Posts Controller', type: :request do
  let(:user) { User.create!(email: 'api@example.com', password: 'password') }
  
  describe 'GET /api/posts' do
    scenario 'returns all posts' do
      Post.create!(title: 'First', user: user)
      Post.create!(title: 'Second', user: user)
      
      get '/api/posts'
      
      expect(response).to have_http_status(:ok)
      expect(json_body.size).to eq(2)
    end
    
    scenario 'requires authentication' do
      get '/api/posts'
      
      expect(response).to have_http_status(:unauthorized)
    end
  end
end

The json_body helper parses the response. This pattern works well for testing REST APIs.

Testing HTTP Requests

For API-heavy applications, test external HTTP calls with WebMock or VCR:

require 'webmock/rspec'

RSpec.describe 'Fetching Weather Data', type: :feature do
  scenario 'displays weather for valid city' do
    stub_request(:get, /api.weather.com/)
      .to_return(json: { temp: 72, condition: 'sunny' })
    
    visit weather_path(city: 'London')
    
    expect(page).to have_content('72F')
    expect(page).to have_content('sunny')
  end
  
  scenario 'handles API error gracefully' do
    stub_request(:get, /api.weather.com/)
      .to_raise(Net::ReadTimeout)
    
    visit weather_path(city: 'London')
    
    expect(page).to have_content('Unable to fetch weather data')
  end
end

WebMock intercepts HTTP requests, letting you test error handling without depending on external services.

Testing Email Delivery

Rails integration tests can verify emails are sent:

RSpec.describe 'Password Reset', type: :feature do
  scenario 'sends reset email' do
    user = User.create!(email: 'reset@example.com', password: 'old')
    
    visit new_password_reset_path
    fill_in 'Email', with: 'reset@example.com'
    click_button 'Send Reset Instructions'
    
    expect(ActionMailer::Base.deliveries.size).to eq(1)
    email = ActionMailer::Base.deliveries.last
    expect(email.to).to include('reset@example.com')
    expect(email.subject).to include('Password Reset')
  end
end

This works with ActionMailer in Rails. For more complex scenarios, use the email_spec gem.

Best Practices for Integration Tests

Keep integration tests focused and maintainable:

Test critical paths only. Not every button click needs an integration test. Focus on user journeys that matter—signup, checkout, search, payment flows.

Keep tests independent. Each test should work in isolation. Do not rely on data created in previous tests. Use let blocks and factories.

Use factories consistently. FactoryBot creates test data reliably. Define traits for common scenarios.

Assert meaningful outcomes. Check what the user sees, not internal state. expect(page).to have_content over expect(user.posts.count).to eq(1).

Clean up after yourself. The database cleaner handles this, but verify tests do not leak state between runs.

Run them in CI but not every save. Integration tests are slower. Run them on CI but use guard or overcommit for local development.

See Also