-
Notifications
You must be signed in to change notification settings - Fork 7
Capybara best practices
Writing Capybara specs can be tricky. Here's some things to keep in mind
It takes a lot of time to start an stop a single capybara spec. If you can test multiple assertions in a single capybara spec, rather than making a separate spec per assertion, then our test suite will be much faster. For instance, if you're adding a new feature to the invite reviewers card, try testing it in a spec which already tests the invite reviewers card rather than making a brand new spec.
Of course there's a balance here. We don't want the entire app tested in a single spec since rerunning and diagnosing any problems will be a hard task. But I think there's a middle ground where we can use each spec to test a set of related features without having overly long and complicated specs.
The way that let
works in rspec means that the code between the braces
is not evaluated until it is first referred to. Usually, this works
well, but consider the following test:
require 'rails_helper'
feature 'test let!', js: true do
let(:user) { FactoryGirl.create :user }
let(:paper) do
FactoryGirl.create :paper_with_phases,
:with_integration_journal,
creator: user
end
before do
login_as(user, scope: :user)
visit '/'
end
scenario 'the paper is listed' do
expect(page).to have_text(paper.title)
end
end
The paper
defined on line 6 will not be evaluated (and inserted into
the database) until the expect
on line 18. At this point, Firefox is
already viewing the page. The test will fail.
Changing let
on line 6 to let!
will correct the issue by eagerly
evaluating, and inserting into the database, the paper at that point.
Note that the user will be evaled (and created) first at line 9, because
the paper is eagerly evaled and the user is required by the paper. It is
best to only use let!
where necessary and let rspec handle evaluation
otherwise.
Rspec allows you to define before
and after
blocks which run before
and after tests. It is important to understand the order in which these
are run.
The canonical before/after block types are :context
(also known
as :all
) and :example
(a/k/a :each
). If you do not specify
which, :example
is the default. They are run in the
order :context
, :example
and if nested, the most specific one is run
last.
Consider the following code:
require 'rails_helper'
feature 'testing before', js: true do
let(:user) { FactoryGirl.create :user }
let!(:paper) { FactoryGirl.create :paper, :with_integration_journal }
before(:example) do
login_as(user, scope: :user)
visit '/'
end
context 'when not assigned to a paper' do
it 'does not display on dashboard' do
expect(page).not_to have_text(paper.title)
end
end
context 'when assigned to a paper' do
before(:example) do
assign_reviewer_role(paper, user)
end
it 'displays on dashboard' do
expect(page).to have_text(paper.title)
end
end
end
In this example, the second test (on line 23) will fail, because the user is not assigned to have a reviewer role until after the page is displayed (on line 9). The most nested before block (starting on line 19) is run last.
A solution to this is to move the visit '/'
code explicitly into
each it
block.
Another solution would be to override the user
binding in the second
context block as
let(:user) do
FactoryGirl.create(:user).tap do |u|
assign_reviewer_role(paper, u)
end
end
This will work because user is evaled inside the top level before
block, ensuring that the user is assigned to the role before the
home page is rendered.
login_as's behaviour when used multiple times (e.g. in parent and child before blocks is not defined). For instance, the following code will fail:
require 'rails_helper'
feature 'login as', js: true do
let(:user) { FactoryGirl.create :user, first_name: 'creator' }
context 'checking login_as' do
let(:paper) do
FactoryGirl.create :paper_with_phases,
:with_integration_journal,
creator: user
end
before do
login_as(user, scope: :user)
visit '/'
end
scenario 'the creator is logged in' do
expect(page).to have_css('.main-nav-user-section-header', text: 'creator')
end
context 'the reviewer is logged in' do
let(:reviewer) { FactoryGirl.create :user, first_name: 'reviewer' }
before do
login_as(reviewer, scope: :user)
end
scenario 'the reviewer is logged in' do
expect(page).to have_css('.main-nav-user-section-header', text: 'reviewer')
end
end
end
end
This will call login_as
multiple times in the test to see if the
reviewer is logged in and will fail.
Instead, use login_as
only once and use rspecs let binding overriding
to set a user. For example:
require 'rails_helper'
feature 'login as', js: true do
let(:creator) { FactoryGirl.create :user, first_name: 'creator' }
context 'checking login_as' do
let(:paper) do
FactoryGirl.create :paper_with_phases,
:with_integration_journal,
creator: creator
end
before do
login_as(user, scope: :user)
visit '/'
end
let(:user) { creator}
scenario 'the creator is logged in' do
expect(page).to have_css('.main-nav-user-section-header', text: 'creator')
end
context 'the reviewer is logged in' do
let(:user) { FactoryGirl.create :user, first_name: 'reviewer' }
scenario 'the reviewer is logged in' do
expect(page).to have_css('.main-nav-user-section-header', text: 'reviewer')
end
end
end
end