# Getting Started with Capybara

[Capybara](https://github.com/teamcapybara/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.

[ad-logs]

## 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](https://rspec.info/) or [Minitest](https://github.com/minitest/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:

```command
mkdir capybara-demo && cd capybara-demo
```

Initialize a Gemfile to manage dependencies:

```command
bundle init
```

Open the newly created `Gemfile` and add the required gems:

```ruby
[label Gemfile]
source 'https://rubygems.org'

[highlight]
gem 'sinatra', '~> 4.0'
gem 'capybara', '~> 3.40'
gem 'rspec', '~> 3.13'
gem 'selenium-webdriver', '~> 4.25'
[/highlight]
```

Install all dependencies by running:

```command
bundle install
```

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

```ruby
[label 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:

```command
mkdir views
```

Add the main page template:

```erb
[label 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:

```erb
[label 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:

```erb
[label 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:

```command
mkdir spec
```

```ruby
[label 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:

```ruby
[label 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:

```command
bundle exec rspec
```

You should see output confirming your test passed:

```text
[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:

```ruby
[label 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:

```ruby
[label 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}"

[highlight]
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
[/highlight]

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:

```ruby
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:

```command
touch spec/features/form_interaction_spec.rb
```

Add the following test code:

```ruby
[label 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:

```command
bundle exec rspec
```

You should see three passing tests:

```text
[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:

```ruby
[label 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

[highlight]
  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
[/highlight]
end
```

Run the tests again:

```command
bundle exec rspec
```

You should now see four passing tests:

```text
[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):

```ruby
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:

```ruby
[label Gemfile]
source 'https://rubygems.org'

gem 'sinatra', '~> 4.0'
gem 'capybara', '~> 3.40'
gem 'rspec', '~> 3.13'
gem 'selenium-webdriver', '~> 4.25'
[highlight]
gem 'puma', '~> 6.4'
[/highlight]
```

Install the gem:

```command
bundle install
```

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

```ruby
[label spec/spec_helper.rb]
require 'capybara/rspec'
[highlight]
require 'selenium-webdriver'
[/highlight]
require_relative '../app'

Capybara.app = Sinatra::Application

# Allow requests to localhost and 127.0.0.1
Capybara.server_host = '127.0.0.1'
[highlight]
# Remove this line: Capybara.app_host = "http://#{Capybara.server_host}"
[/highlight]

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
[highlight]
Capybara.default_max_wait_time = 5
[/highlight]

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

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

```ruby
[label 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

[highlight]
get '/delayed' do
  erb :delayed
end
[/highlight]
```

Create a new view that simulates delayed content:

```erb
[label 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:

```command
touch spec/features/waiting_spec.rb
```

Add the test code:

```ruby
[label 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:

```command
bundle exec rspec spec/features/waiting_spec.rb
```

You should see the test pass:

```text
[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`:

```ruby
[label 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

  [highlight]
  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
  [/highlight]
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:

```command
bundle exec rspec spec/features/waiting_spec.rb
```

```text
[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:

```erb
[label views/delayed.erb]
<!DOCTYPE html>
<html>
<head>
  <title>Delayed Content</title>
  <script>
    window.onload = function() {
      setTimeout(function() {
        [highlight]
        document.getElementById('loading').style.display = 'none';
        [/highlight]
        document.getElementById('delayed-message').textContent = 'Content Loaded!';
        document.getElementById('delayed-message').style.display = 'block';
      }, 1000);
    };
  </script>
</head>
<body>
  <h1>Waiting for Content</h1>
  [highlight]
  <div id="loading">Loading...</div>
  [/highlight]
  <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:

```ruby
[label 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

  [highlight]
  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
  [/highlight]
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:

```command
bundle exec rspec spec/features/waiting_spec.rb
```

```text
[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:

```ruby
# 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](https://rubydoc.info/github/teamcapybara/capybara/master).

Thanks for reading, and happy testing!