Capybara Testing Guide: Browser Tests in Ruby

Capybara Testing Guide: Browser Tests in Ruby

Capybara is the standard Ruby library for simulating browser interactions in tests. It's the engine behind Rails system specs and RSpec feature specs. Instead of asserting on raw HTML, you interact with your app the way a user would: clicking buttons, filling forms, navigating pages.

How Capybara Works

Capybara drives your app through a driver — a backend that actually runs the browser or simulates one:

Driver Speed JavaScript Use for
rack_test Fastest No Non-JS pages
selenium_chrome_headless Slow Yes Full JS apps
cuprite (Ferrum) Fast Yes Chrome DevTools Protocol

The default driver is rack_test. For JavaScript-heavy tests, switch to Selenium or Cuprite.

Installation

# Gemfile
group :test do
  gem 'capybara'
  gem 'selenium-webdriver'  # for JS testing
  gem 'webdrivers'          # auto-downloads ChromeDriver
end

For Rails with RSpec, capybara is already included in rspec-rails dependencies. Just require 'capybara/rspec'.

Basic Navigation

# Visit a URL
visit '/products'
visit root_path
visit product_path(@product)

# Check the current page
expect(current_path).to eq('/products')
expect(page).to have_current_path('/products')

Finding Elements

Capybara provides a rich set of finders:

# By CSS selector
find('.product-title')
find('#submit-button')
find('input[name="email"]')

# By text
find('button', text: 'Submit')
find('a', text: 'Learn more')

# Semantic finders (preferred)
find_button('Submit')
find_link('Learn more')
find_field('Email')

# Scoped search
within('.product-list') do
  find('.product-title', text: 'Widget')
end

# Find all matching elements
all('.product-card')
all('li').map(&:text)

Always prefer semantic finders over CSS selectors when possible — they're more resilient to HTML changes.

Interacting with Pages

Clicking

click_link 'Sign In'
click_button 'Submit'
click_on 'Save Changes'      # clicks links or buttons
click_link_or_button 'Next'  # same as click_on

Forms

fill_in 'Email', with: 'alice@example.com'
fill_in 'Password', with: 'secret123'

fill_in 'Search', with: ''   # clear a field

select 'Admin', from: 'Role'
choose 'Female'              # radio button
check 'Agree to terms'
uncheck 'Subscribe to newsletter'

attach_file 'Avatar', '/path/to/image.png'

Form submission

click_button 'Create Account'
# or
find('form').native.submit

Assertions

Capybara matchers work with RSpec's expect:

# Text content
expect(page).to have_text('Welcome back!')
expect(page).to have_content('Order confirmed')   # alias
expect(page).not_to have_text('Error')

# Elements
expect(page).to have_selector('.flash-message')
expect(page).to have_css('.product-card', count: 5)
expect(page).not_to have_selector('.spinner')

# Links and buttons
expect(page).to have_link('Sign Out')
expect(page).to have_button('Submit')
expect(page).to have_button('Submit', disabled: true)

# Form fields
expect(page).to have_field('Email', with: 'alice@example.com')
expect(page).to have_select('Role', selected: 'Admin')
expect(page).to have_checked_field('Agree to terms')
expect(page).to have_unchecked_field('Subscribe')

# Table
expect(page).to have_table('Orders', with_rows: [['Order 1', '$10.00']])

JavaScript Testing

Switch the driver for tests that require JavaScript:

# In the spec file
RSpec.describe 'Dynamic search', type: :system do
  before { driven_by(:selenium_chrome_headless) }

  it 'filters results as you type' do
    visit '/products'
    fill_in 'Search', with: 'widget'
    expect(page).to have_text('Widget Pro')
    expect(page).not_to have_text('Gadget X')
  end
end

Or add :js metadata to automatically switch drivers:

it 'shows a confirmation modal', :js do
  click_button 'Delete'
  expect(page).to have_text('Are you sure?')
  click_button 'Confirm'
  expect(page).not_to have_selector('.modal')
end

Configure the JS driver in spec/support/capybara.rb:

Capybara.register_driver :selenium_chrome_headless do |app|
  options = Selenium::WebDriver::Chrome::Options.new
  options.add_argument('--headless')
  options.add_argument('--no-sandbox')
  options.add_argument('--disable-dev-shm-usage')
  Capybara::Selenium::Driver.new(app, browser: :chrome, options: options)
end

Capybara.javascript_driver = :selenium_chrome_headless

Waiting Behavior

Capybara automatically retries finders and matchers for up to 2 seconds (default). This handles async UI updates without explicit sleeps:

click_button 'Load More'
expect(page).to have_css('.product-card', count: 20)  # waits for AJAX

Adjust the timeout for slow operations:

Capybara.default_max_wait_time = 5  # seconds

For a specific wait:

expect(page).to have_text('Uploaded', wait: 10)

Never use sleep in Capybara tests. It makes tests slow and brittle. Rely on Capybara's built-in waiting instead.

Scoping with within

within restricts all actions and finders to a DOM subtree:

within('#user-profile') do
  fill_in 'Name', with: 'Bob'
  click_button 'Update'
end

within('table tbody tr', text: 'Order #123') do
  click_link 'View'
end

This prevents false positives when multiple similar elements exist on a page.

Debugging Failing Tests

save_and_open_page

Opens the current page HTML in your browser:

it 'shows the product list' do
  visit '/products'
  save_and_open_page  # opens browser with current DOM
  expect(page).to have_css('.product-card')
end

Requires the launchy gem.

save_screenshot

save_screenshot('debug.png')

Logging

page.html     # raw HTML
page.text     # visible text
page.title    # page title
puts page.text

Helper Methods

Extract common interactions into helper modules:

# spec/support/helpers/session_helpers.rb
module SessionHelpers
  def sign_in_as(user)
    visit sign_in_path
    fill_in 'Email', with: user.email
    fill_in 'Password', with: 'password'
    click_button 'Sign In'
  end
end

RSpec.configure do |config|
  config.include SessionHelpers, type: :system
end

Use in specs:

RSpec.describe 'Dashboard', type: :system do
  let(:user) { create(:user) }

  before { sign_in_as(user) }

  it 'shows the welcome message' do
    expect(page).to have_text("Welcome, #{user.name}")
  end
end

Common Patterns

Testing Flash Messages

click_button 'Save'
expect(page).to have_css('.flash.success', text: 'Changes saved')

Testing Redirects

click_link 'Dashboard'
expect(current_path).to eq(dashboard_path)

Testing Pagination

expect(page).to have_selector('.product-card', count: 25)
click_link 'Next'
expect(page).to have_selector('.product-card', count: 15)

Testing Modals

click_button 'Delete'
within('.modal') do
  expect(page).to have_text('Are you sure?')
  click_button 'Confirm'
end
expect(page).not_to have_selector('.modal')

What Capybara Is Not For

Capybara is best for user-facing behavior. Don't use it to test:

  • API endpoints (use request specs instead)
  • Business logic (use model specs)
  • Jobs or mailers (use dedicated spec types)

Reserve Capybara for critical user journeys: sign in, create/edit/delete records, checkout flows. Keep the number of system specs small and the assertions meaningful — each test should verify something a user cares about, not just that HTML elements exist.

Read more