Skip to content

Capybara best practices

Erik Hetzner edited this page Apr 18, 2018 · 2 revisions

Capybara best practices

Writing Capybara specs can be tricky. Here's some things to keep in mind

Avoid writing too many specs

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.

Use let! not let when creating necessary database objects

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.

Understand before and after precedence

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.

Use login_as only once per spec file

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
Clone this wiki locally