Capybara Test Helpers is an opinionated library built on top of capybara, that encourages good testing practices based on encapsulation and reuse.
Write tests that everyone can understand, and leverage your Ruby skills to keep them easy to read and easy to change.
capybara
is a great library for integration tests in Ruby,
commonly used in combination with RSpec or cucumber.
Although cucumber encourages good practices such as writing steps at a high level, thinking in terms of the user rather than the interactions required, it doesn't scale well in a large project. Steps are available for all tests, and there's no way to partition or isolate them.
At the same time, Gherkin is very limited as a language, it can be very awkward to use when steps require parameters, and it's hard to find and detect duplicate steps, and very time consuming to refactor them.
In contrast, writing tests in RSpec has a very low barrier since Ruby is a joy to work with, but you are on your own to encapsulate code to avoid coupling tests to the current UI. Small changes to the UI should not require rewriting dozens of tests, but without clear guidelines it's hard to achieve good tests.
This library provides a solid foundation of simple and repeatable patterns that can be used to write better tests.
- Leverage your Ruby skills for keeping tests in good shape
- Powerful syntax for assertions (without monkey patching)
- Aliases for element locators to avoid repetition
- Composability: define interactions with your UI once, and [focus on the tests][testing robots] many times
- Dependency injection to make tests predictable and robust
- Full access to the Capybara DSL
Add this line to your application's Gemfile:
gem 'capybara_test_helpers'
And then run:
$ bundle install
To use with RSpec, require the following in spec_helper.rb
:
require 'capybara_test_helpers/rspec'
If using Rails, make sure you follow the setup in rspec-rails
first.
You can run rails g test_helper base
to create a base test helper and require
it as well so that other test helpers can extend it without manually requiring.
# spec/rails_helper.rb
require 'capybara_test_helpers/rspec'
require Rails.root.join('test_helpers/base_test_helper')
Check this example to see how you can get started.
To use with Cucumber, require the following in env.rb
:
require 'capybara_test_helpers/cucumber'
require Rails.root.join('test_helpers/base_test_helper')
Have in mind that RSpec is a much better fit, as Gherkin is very limited.
That said, test helpers do provide a nice way to share code if you are migrating from Cucumber to RSpec.
Check this example to see how you can get started.
You can define a test helper by subclassing Capybara::TestHelper
, which has
full access to the Capybara DSL.
class UsersTestHelper < Capybara::TestHelper
# Selectors: Semantic aliases for elements, a useful abstraction.
SELECTORS = {
el: 'table.users',
form: '.user-form',
submit_button: [:button, type: 'submit'],
}
# Getters: A convenient way to get related data or nested elements.
def row_for(user)
within { find(:table_row, { 'Name' => user.name }) }
end
# Actions: Encapsulate complex actions to provide a cleaner interface.
def add(attrs)
click_on('Add User')
save_user(**attrs)
end
def edit(user, with:)
row_for(user).click_on('Edit')
save_user(**with)
end
def delete(user)
accept_confirm { row_for(user).click_on('Delete') }
end
private \
def save_user(name:, language:)
within(:form) {
fill_in('Name', with: name)
choose('Language', option: language)
submit_button.click
}
end
# Assertions: Allow to check on element properties while keeping it DRY.
def have_user(name:, language:)
columns = { 'Name' => name, 'Language' => language }
within { have(:table_row, columns) }
end
end
When using Rails, you can generate a test helper by running:
$ rails g test_helper users
You can find this working example and more in the example app and the Capybara tests.
require 'rails_helper'
RSpec.describe 'Cities', test_helpers: [:cities] do
let!(:nyc) { cities.given_there_is_a_city('NYC') }
before { cities.visit_page }
scenario 'valid inputs' do
cities.add(name: 'Minneapolis')
cities.should.have_city('Minneapolis')
end
scenario 'invalid inputs' do
cities.add(name: '') { |form|
form.should.have_error("Name can't be blank")
}
end
scenario 'editing a city' do
cities.edit(nyc, with: { name: 'New York City' })
cities.should_no_longer.have_city('NYC')
cities.should_now.have_city('New York City')
end
scenario 'deleting a city', screen_size: :phone do
cities.delete(nyc)
cities.should_no_longer.have_city('NYC')
end
end
To make the test helper available you can use the test_helpers
option
in a describe
, context
or scenario
as seen above.
When using Cucumber, you may call use_test_helpers
in the step definitions.
Finally, for test helpers that you expect to use very often, you can use_test_helpers
in an RSpec helper module to make them available globally.
A documentation website with the full API and examples is coming soon
Every single method in the Capybara DSL is available inside test helpers, as well as the built-in RSpec matchers.
You can encapsulate locators for commonly used elements to avoid hardcoding them in different tests.
As a result, if the implementation changes, there are less places that need to be updated, making it faster to update tests after UI changes.
class FormTestHelper < BaseTestHelper
SELECTORS = {
el: '.form',
error_summary: ['#error_explanation', visible: true],
name_input: [:fillable_field, 'Name'],
save_button: [:button, type: 'submit'],
}
You can then leverage these aliases on any Capybara method:
# Finding an element
form.find(:save_button, visible: false)
# Interacting with an element
form.fill_in(:name_input, with: 'Jane')
# Making an assertion
form.has_selector?(:error_summary, text: "Can't be blank")
To avoid repetition, getters are available for every selector alias:
form.find(:name_input)
# same as
form.name_input
form.find(:error_summary, text: "Can't be blank")
# same as
form.error_summary(text: "Can't be blank")
By convention, :el
is the top-level element of the component or page the test
helper is encapsulating, which will be used automatically when calling a
Capybara operation that requires a node, such as click
or value
.
form.within { save_button.click }
# same as
form.within(:el) { save_button.click }
# same as
form.el.within { save_button.click }
You can use any of the RSpec matchers provided by Capybara, but the way to use them in test helpers is slightly different.
Before using an assertion, you must call should
or should_not
, and then
chain the RSpec matcher or your own custom assertion.
users.find(:table)
.should.have_selector(:table_row, ['Jane', 'Doe']
.should_not.have_selector(:table_row, ['John', 'Doe'])
The example above becomes a lot nicer if we define a more semantic assertion, which can be easily done by leveraging an existing assertion:
class UsersTestHelper < BaseTestHelper
SELECTORS = {
list: 'table.users',
}
# Assertions: Check on element properties, used with `should` and `should_not`.
def have_user(*names)
have(:table_row, names)
end
and then use it as:
users.list
.should.have_user('Jane', 'Doe')
.should_not.have_user('John', 'Doe')
Notice that you don't need to define both the positive and negative assertions, they are both available because we are using an existing assertion.
Sometimes built-in assertions are not enough, and you need to use an expectation
directly. Test helpers provide to_or
and not_to
methods that you
can use to implement an assertion that you can use with should
or should_not
.
class CurrentPageTestHelper < BaseTestHelper
# Getters: A convenient way to get related data or nested elements.
def fullscreen?
evaluate_script('!!(document.mozFullScreenElement || document.webkitFullscreenElement)')
end
# Assertions: Allow to check on element properties while keeping it DRY.
def be_fullscreen
expect(fullscreen?).to_or not_to, eq(true)
end
end
current_page.should.be_fullscreen
current_page.should_not.be_fullscreen
You can make the assertion retry automatically until the Capybara timeout by
using synchronize_expectation
:
def be_fullscreen
synchronize_expectation {
expect(fullscreen?).to_or not_to, eq(true)
}
end
This library is loosely based on the concepts of Page Objects and Testing Robots, with a healthy dose of dependency injection.
Capybara has a great DSL, so the focus of this library is to build upon it, by
allowing you to create your own actions and assertions and call them just as
fluidly as you would call find
or has_content?
.
This library works best when encapsulating common UI patterns in separate helpers,
such as a FormTestHelper
or a DropdownTestHelper
, and then reusing them in
page-specific test helpers to make the test read more semantically.
Regarding selectors, I highly recommend writing one attribute per line, sorting them alphabetically (most editors can do it for you), and always using a trailing comma.
class DropdownTestHelper < BaseTestHelper
# Selectors: Semantic aliases for elements, a useful abstraction.
SELECTORS = {
el: '.dropdown',
toggle: '.dropdown-toggle',
}
It will minimize the amount of git conflicts, and keep the history a lot cleaner and more meaningful when using git blame
.
This library wouldn't be the same without the early validation from my colleagues, and numerous improvements and bugfixes they contributed to it. Thanks for the support 😃
- capybara: Solid library to write integration tests in Ruby.
The gem is available as open source under the terms of the MIT License.