Back to Scaling Ruby Applications guides

Getting Started with Capybara

Stanley Ulili
Updated on October 7, 2025

Capybara is a robust testing framework for Ruby that enables you to write integration and acceptance tests by simulating how real users interact with your web application. Its intuitive API and extensive browser automation capabilities have made it the de facto standard for testing Rails applications, though it works equally well with Sinatra, Hanami, and other Ruby web frameworks.

This tutorial will walk you through building a comprehensive testing suite for your Ruby web application using Capybara.

Prerequisites

Before working through this tutorial, make sure you have Ruby (version 3.0 or higher recommended) installed on your system. You should also have a basic understanding of Ruby syntax and testing concepts. This article assumes you're familiar with RSpec or Minitest, as we'll be using these frameworks alongside Capybara.

Setting up your first Capybara project

To demonstrate Capybara's capabilities effectively, we'll create a simple Sinatra application that you can use to follow along. Begin by creating a new directory for the project:

 
mkdir capybara-demo && cd capybara-demo

Initialize a Gemfile to manage dependencies:

 
bundle init

Open the newly created Gemfile and add the required gems:

Gemfile
source 'https://rubygems.org'

gem 'sinatra', '~> 4.0'
gem 'capybara', '~> 3.40'
gem 'rspec', '~> 3.13'
gem 'selenium-webdriver', '~> 4.25'

Install all dependencies by running:

 
bundle install

Create a simple Sinatra application in an app.rb file:

app.rb
require 'sinatra'

get '/' do
  erb :index
end

post '/submit' do
  @name = params[:name]
  @email = params[:email]
  erb :result
end

get '/about' do
  erb :about
end

Set up the corresponding view templates. First, create a views directory:

 
mkdir views

Add the main page template:

views/index.erb
<!DOCTYPE html>
<html>
<head>
  <title>Capybara Demo</title>
</head>
<body>
  <h1>Welcome to Capybara Testing</h1>

  <form action="/submit" method="POST">
    <label for="name">Name:</label>
    <input type="text" id="name" name="name" required>

    <label for="email">Email:</label>
    <input type="email" id="email" name="email" required>

    <button type="submit">Submit Form</button>
  </form>

  <a href="/about">About Page</a>
</body>
</html>

Create the result page that displays form submissions:

views/result.erb
<!DOCTYPE html>
<html>
<head>
  <title>Form Submitted</title>
</head>
<body>
  <h1>Thank You!</h1>
  <p>Name: <%= @name %></p>
  <p>Email: <%= @email %></p>
  <a href="/">Back to Home</a>
</body>
</html>

Add an about page:

views/about.erb
<!DOCTYPE html>
<html>
<head>
  <title>About</title>
</head>
<body>
  <h1>About This Application</h1>
  <p>This is a demonstration application for Capybara testing.</p>
  <a href="/">Home</a>
</body>
</html>

Now configure RSpec and Capybara by creating a spec/spec_helper.rb file:

 
mkdir spec
spec/spec_helper.rb
require 'capybara/rspec'
require_relative '../app'

Capybara.app = Sinatra::Application

# Allow requests to localhost and 127.0.0.1
Capybara.server_host = '127.0.0.1'
Capybara.app_host = "http://#{Capybara.server_host}"

RSpec.configure do |config|
  config.include Capybara::DSL
end

This configuration tells Capybara which application to test and integrates it with RSpec. The Capybara.app assignment points to your Sinatra application, while config.include Capybara::DSL makes Capybara's methods available in your tests.

Create your first test file:

spec/features/homepage_spec.rb
require 'spec_helper'

RSpec.describe 'Homepage', type: :feature do
  it 'displays the welcome message' do
    visit '/'
    expect(page).to have_content('Welcome to Capybara Testing')
  end
end

Run the test suite:

 
bundle exec rspec

You should see output confirming your test passed:

Output
.

Finished in 0.01252 seconds (files took 0.37013 seconds to load)
1 example, 0 failures

The test you just wrote demonstrates Capybara's fundamental workflow: visit a page, interact with it, and verify the expected outcome. The visit method loads the specified URL, while have_content checks if the page contains specific text.

Understanding Capybara's driver architecture

Capybara's flexibility comes from its driver system, which allows you to switch between different browser automation backends without rewriting your tests. Each driver offers different capabilities and trade-offs in terms of speed, JavaScript support, and debugging features.

The default driver is :rack_test, which provides extremely fast test execution by bypassing actual browser rendering. This driver directly tests your Rack application without JavaScript support, making it ideal for testing server-rendered pages and basic interactions:

spec/features/navigation_spec.rb
require 'spec_helper'

RSpec.describe 'Navigation', type: :feature do
  it 'navigates to the about page' do
    visit '/'
    click_link 'About Page'
    expect(page).to have_content('About This Application')
    expect(current_path).to eq('/about')
  end
end

This test runs in milliseconds because :rack_test doesn't start an actual browser. However, if your application relies heavily on JavaScript, you'll need a driver that supports it.

Configuring Selenium WebDriver

For testing JavaScript functionality, Capybara integrates seamlessly with Selenium WebDriver. You need to register a custom Selenium driver in your spec helper file.

Open your spec/spec_helper.rb file and add the Selenium configuration below the existing Capybara setup:

spec/spec_helper.rb
require 'capybara/rspec'
require_relative '../app'

Capybara.app = Sinatra::Application

# Allow requests to localhost and 127.0.0.1
Capybara.server_host = '127.0.0.1'
Capybara.app_host = "http://#{Capybara.server_host}"

Capybara.register_driver :selenium_chrome_headless do |app|
options = Selenium::WebDriver::Chrome::Options.new
options.add_argument('--headless')
options.add_argument('--disable-gpu')
options.add_argument('--no-sandbox')
Capybara::Selenium::Driver.new(app, browser: :chrome, options: options)
end
Capybara.javascript_driver = :selenium_chrome_headless
RSpec.configure do |config| config.include Capybara::DSL end

This configuration creates a custom Selenium driver that runs Chrome in headless mode. The headless option means the browser runs without a graphical interface, significantly speeding up test execution while still providing full JavaScript support.

You can mark individual tests to use the JavaScript driver by adding js: true metadata:

 
it 'handles dynamic content', js: true do
  visit '/dynamic-page'
  # This test will use Selenium instead of rack_test
end

Choosing the right driver

Different scenarios call for different drivers. Use :rack_test for:

  • Testing server-rendered HTML pages
  • Form submissions without JavaScript
  • Basic navigation and link clicking
  • Scenarios where maximum speed is critical

Switch to Selenium (or similar JavaScript-capable drivers) when:

  • Testing single-page applications
  • Verifying JavaScript-driven interactions
  • Testing AJAX requests and responses
  • Debugging complex browser behavior

The ability to mix drivers within the same test suite gives you the best of both worlds: fast execution for simple tests and comprehensive JavaScript support when needed.

Finding and interacting with page elements

Capybara provides multiple strategies for locating elements on your pages, each with specific use cases and advantages. Understanding when to use each finder helps you write more reliable and maintainable tests.

Using semantic finders

The most robust approach involves using Capybara's semantic finders, which locate elements based on their purpose rather than implementation details.

Create a new test file to demonstrate form interactions:

 
touch spec/features/form_interaction_spec.rb

Add the following test code:

spec/features/form_interaction_spec.rb
require 'spec_helper'

RSpec.describe 'Form Interaction', type: :feature do
  it 'submits the contact form successfully' do
    visit '/'

    fill_in 'Name', with: 'Jane Smith'
    fill_in 'Email', with: 'jane@example.com'
    click_button 'Submit Form'

    expect(page).to have_content('Thank You!')
    expect(page).to have_content('Jane Smith')
    expect(page).to have_content('jane@example.com')
  end
end

Run your tests to verify everything works:

 
bundle exec rspec

You should see three passing tests:

Output
...

Finished in 0.02507 seconds (files took 0.37043 seconds to load)
3 examples, 0 failures

This approach uses the visible labels on your form rather than IDs or CSS classes. The fill_in method can locate inputs by their label text, placeholder, or name attribute, making your tests resilient to implementation changes.

Alternative element selectors

When semantic finders aren't sufficient, Capybara offers several other locator strategies. Open your existing spec/features/form_interaction_spec.rb file and add a new test to demonstrate these different approaches:

spec/features/form_interaction_spec.rb
require 'spec_helper'

RSpec.describe 'Form Interaction', type: :feature do
  it 'submits the contact form successfully' do
    visit '/'

    fill_in 'Name', with: 'Jane Smith'
    fill_in 'Email', with: 'jane@example.com'
    click_button 'Submit Form'

    expect(page).to have_content('Thank You!')
    expect(page).to have_content('Jane Smith')
    expect(page).to have_content('jane@example.com')
  end

it 'demonstrates different selector types' do
visit '/'
# Find by ID
find('#name').set('John Doe')
# Find by CSS selector
find('input[type="email"]').set('john@example.com')
# Find by XPath
find(:xpath, '//button[@type="submit"]').click
expect(page).to have_content('Thank You!')
expect(page).to have_content('John Doe')
end
end

Run the tests again:

 
bundle exec rspec

You should now see four passing tests:

Output
....

Finished in 0.03164 seconds (files took 0.41572 seconds to load)
4 examples, 0 failures

Using data- attributes specifically for testing is often recommended because they make your selectors immune to styling changes while clearly documenting which elements are used in tests. For example, you could add data-test-id="submit-button" to your button and find it with find('[data-test-id="submit"]').

Scoping interactions

You can scope your interactions to specific sections of the page using within blocks, which is particularly useful for complex pages with repeating elements. The within block ensures that Capybara only searches for elements inside the specified container, preventing accidental interactions with similarly-named elements elsewhere on the page.

Here's an example that demonstrates scoping (this is just an illustration—you don't need to add this to your test suite right now):

 
it 'interacts with elements within a specific section' do
  visit '/dashboard'

  within('.user-profile') do
    expect(page).to have_content('Profile Information')
    click_link 'Edit Profile'
  end

  within('#edit-form') do
    fill_in 'Username', with: 'newusername'
    click_button 'Save Changes'
  end
end

Handling asynchronous behavior and waiting

One of Capybara's most valuable features is its automatic waiting mechanism, which dramatically reduces flaky tests caused by timing issues. Understanding how this works helps you write more reliable tests.

Automatic waiting behavior

By default, Capybara waits up to 2 seconds for elements to appear before failing. This happens transparently whenever you use finder methods. Let's see this in action by creating a page with delayed content.

First, update your Gemfile to add the puma gem:

Gemfile
source 'https://rubygems.org'

gem 'sinatra', '~> 4.0'
gem 'capybara', '~> 3.40'
gem 'rspec', '~> 3.13'
gem 'selenium-webdriver', '~> 4.25'
gem 'puma', '~> 6.4'

Install the gem:

 
bundle install

Now update your spec/spec_helper.rb to require selenium-webdriver:

spec/spec_helper.rb
require 'capybara/rspec'
require 'selenium-webdriver'
require_relative '../app' Capybara.app = Sinatra::Application # Allow requests to localhost and 127.0.0.1 Capybara.server_host = '127.0.0.1'
# Remove this line: Capybara.app_host = "http://#{Capybara.server_host}"
Capybara.register_driver :selenium_chrome_headless do |app| options = Selenium::WebDriver::Chrome::Options.new options.add_argument('--headless') options.add_argument('--disable-gpu') options.add_argument('--no-sandbox') Capybara::Selenium::Driver.new(app, browser: :chrome, options: options) end Capybara.javascript_driver = :selenium_chrome_headless
Capybara.default_max_wait_time = 5
RSpec.configure do |config| config.include Capybara::DSL end

Update your app.rb file to add a new route:

app.rb
require 'sinatra'

get '/' do
  erb :index
end

post '/submit' do
  @name = params[:name]
  @email = params[:email]
  erb :result
end

get '/about' do
  erb :about
end

get '/delayed' do
erb :delayed
end

Create a new view that simulates delayed content:

views/delayed.erb
<!DOCTYPE html>
<html>
<head>
  <title>Delayed Content</title>
  <script>
    window.onload = function() {
      setTimeout(function() {
        document.getElementById('delayed-message').textContent = 'Content Loaded!';
        document.getElementById('delayed-message').style.display = 'block';
      }, 1000);
    };
  </script>
</head>
<body>
  <h1>Waiting for Content</h1>
  <div id="delayed-message" style="display:none;"></div>
  <a href="/">Home</a>
</body>
</html>

This page uses JavaScript to display content after a 1-second delay, simulating a typical AJAX request.

Now create a test for this delayed content:

 
touch spec/features/waiting_spec.rb

Add the test code:

spec/features/waiting_spec.rb
require 'spec_helper'

RSpec.describe 'Automatic Waiting', type: :feature do
  it 'waits for delayed content automatically', js: true do
    visit '/delayed'

    # Capybara will automatically wait up to 5 seconds for this content
    expect(page).to have_content('Content Loaded!')
  end
end

Run the tests:

 
bundle exec rspec spec/features/waiting_spec.rb

You should see the test pass:

Output
Capybara starting Puma...
* Version 6.6.1, codename: Return to Forever
* Min threads: 0, max threads: 4
* Listening on http://127.0.0.1:61804
127.0.0.1 - - [07/Oct/2025:15:07:57 +0200] "GET /delayed HTTP/1.1" 200 474 0.0066
127.0.0.1 - - [07/Oct/2025:15:07:57 +0200] "GET /favicon.ico HTTP/1.1" 404 441 0.0004
.

Finished in 2.59 seconds (files took 0.38787 seconds to load)
1 example, 0 failures

Notice how Capybara automatically started a Puma server to host your application during testing. The test took about 2.6 seconds to complete. Capybara automatically waited for the content to appear without any explicit sleep commands.

Adjusting wait times for specific tests

For individual scenarios requiring different timeouts, use the using_wait_time helper. Add this test to your waiting_spec.rb:

spec/features/waiting_spec.rb
require 'spec_helper'

RSpec.describe 'Automatic Waiting', type: :feature do
  it 'waits for delayed content automatically', js: true do
    visit '/delayed'

    # Capybara will automatically wait up to 5 seconds for this content
    expect(page).to have_content('Content Loaded!')
  end

it 'uses custom wait time for slow operations', js: true do
visit '/delayed'
using_wait_time(10) do
expect(page).to have_content('Content Loaded!')
end
end
end

The using_wait_time block temporarily overrides the default wait time for operations that need more patience, like slow API calls or complex animations.

Run the tests again:

 
bundle exec rspec spec/features/waiting_spec.rb
Output
Capybara starting Puma...
* Version 6.6.1, codename: Return to Forever
* Min threads: 0, max threads: 4
* Listening on http://127.0.0.1:61911
127.0.0.1 - - [07/Oct/2025:15:13:25 +0200] "GET /delayed HTTP/1.1" 200 474 0.0048
127.0.0.1 - - [07/Oct/2025:15:13:25 +0200] "GET /favicon.ico HTTP/1.1" 404 441 0.0005
.127.0.0.1 - - [07/Oct/2025:15:13:26 +0200] "GET /delayed HTTP/1.1" 200 474 0.0009
.

Finished in 3.91 seconds (files took 0.52269 seconds to load)
2 examples, 0 failures

You should see two passing tests. The using_wait_time block temporarily overrides the default 5-second wait time we set globally. This is useful for operations that need more patience, like slow API calls or complex animations. In this case, we increased the wait time to 10 seconds, though our delayed content still loads in just 1 second. If you had content that took longer to load, this extended timeout would prevent premature test failures.

Waiting for elements to disappear

Capybara can also wait for elements to disappear, which is useful when testing loading indicators. Update your views/delayed.erb to include a loading spinner:

views/delayed.erb
<!DOCTYPE html>
<html>
<head>
  <title>Delayed Content</title>
  <script>
    window.onload = function() {
      setTimeout(function() {
document.getElementById('loading').style.display = 'none';
document.getElementById('delayed-message').textContent = 'Content Loaded!'; document.getElementById('delayed-message').style.display = 'block'; }, 1000); }; </script> </head> <body> <h1>Waiting for Content</h1>
<div id="loading">Loading...</div>
<div id="delayed-message" style="display:none;"></div> <a href="/">Home</a> </body> </html>

The page now shows a loading message that disappears when the content loads.

Add a test that waits for the loading indicator to disappear:

spec/features/waiting_spec.rb
require 'spec_helper'

RSpec.describe 'Automatic Waiting', type: :feature do
  it 'waits for delayed content automatically', js: true do
    visit '/delayed'

    expect(page).to have_content('Content Loaded!')
  end

  it 'uses custom wait time for slow operations', js: true do
    visit '/delayed'

    using_wait_time(10) do
      expect(page).to have_content('Content Loaded!')
    end
  end

it 'waits for loading indicator to disappear', js: true do
visit '/delayed'
# First verify the loading indicator is present
expect(page).to have_content('Loading...')
# Then wait for it to disappear
expect(page).to have_no_content('Loading...')
# Finally verify the content appears
expect(page).to have_content('Content Loaded!')
end
end

The have_no_content matcher automatically waits for the element to disappear, making it perfect for testing loading states.

Run all the waiting tests:

 
bundle exec rspec spec/features/waiting_spec.rb
Output
...

Finished in 6.78934 seconds (files took 0.54123 seconds to load)
3 examples, 0 failures

Common waiting pitfalls

Avoid using Ruby's sleep in your tests, as it introduces arbitrary delays that make your suite slower and more brittle:

 
# Bad: Fixed delay
it 'uses sleep (avoid this)' do
  visit '/page'
  click_button 'Load'
  sleep 3  # Don't do this!
  expect(page).to have_content('Loaded')
end

# Good: Dynamic waiting
it 'uses Capybara waiting' do
  visit '/page'
  click_button 'Load'
  expect(page).to have_content('Loaded')  # Waits automatically
end

The Capybara approach is both faster (no unnecessary waiting) and more reliable (adapts to varying server response times).

Final thoughts

Throughout this tutorial, you've built a solid foundation for testing Ruby web applications with Capybara. You've learned how to set up the framework, configure drivers for different testing scenarios, interact with page elements using semantic finders, and handle asynchronous behavior with automatic waiting mechanisms.

The concepts covered here provide the essential tools for writing effective integration tests. By simulating real user interactions, Capybara helps you catch issues that unit tests might miss while keeping your tests readable and maintainable.

For more advanced usage and detailed API documentation, consult the official Capybara documentation.

Thanks for reading, and happy testing!

Got an article suggestion? Let us know
Next article
Top 10 Ruby on Rails Alternatives for Web Development
Discover the top 10 Ruby on Rails alternatives. Compare Django, Laravel, Express.js, Spring Boot & more with detailed pros, cons & features.
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.