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
endFor 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_onForms
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.submitAssertions
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
endOr 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')
endConfigure 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_headlessWaiting 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 AJAXAdjust the timeout for slow operations:
Capybara.default_max_wait_time = 5 # secondsFor 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'
endThis 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')
endRequires the launchy gem.
save_screenshot
save_screenshot('debug.png')Logging
page.html # raw HTML
page.text # visible text
page.title # page titlePrint all visible text
puts page.textHelper 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
endUse 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
endCommon 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.