Getting Started with Capybara
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:
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:
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:
<!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:
<!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:
<!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
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:
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:
.
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:
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:
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:
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:
...
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:
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:
....
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:
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:
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:
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:
<!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:
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:
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
:
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
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:
<!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:
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
...
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!