Capybara is one of those tools that sounds great but is often frustrating. The claims of ‘no setup’ and ‘intuitive API’ make it sound like automating your browser testing is going to be a simple task. Unfortunately, the nature of these full-stack tests mean they’re often very tricky to get working reliably, and this has always put me off before. Testing should save you time, not create extra work.

This weekend, starting on a new test suite, I decided to go about really getting a solid Capybara setup, and get a complex test passing every time without anysleep hacks. Here’s how I did it.

1. Prerequisites

I used the following gems (add them to your Gemfile):

  • rspec-rails
  • capybara
  • database_cleaner
  • poltergeist
  • factory_girl_rails

You might also want to install phantomjs with your package manager if poltergeist doesn’t install first time.

Install with bundle install and then rails g rspec:install.

2. Test authentication

One of the first problems is how to get your ‘logged-in’ tests working. Devise’s regular helpers don’t work with Capybara (it doesn’t have access to the Rails request), so I used Warden.test_mode! instead:

In spec/rails_helper.rb:

include Warden::Test::Helpers
Warden.test_mode!

RSpec.configure do |config|
  # Fast login for tests that specify 'login: true'
  config.before(:each, login: true) do
    @user = FactoryGirl.create(:user)
    login_as(@user, scope: :user)
  end
end

This creates a user with FactoryGirl and logs them in before every test that has login: true, eg:

describe 'My test', login: true, js: true do

3. Database cleaner

Since Capybara runs in a separate thread, and your tests run within a database transaction, the Capybara browser won’t see your database changes without some extra effort. My solution was to disable transactional fixtures and configure database_cleaner as described in this post.

In spec/support/database_cleaner.rb:

RSpec.configure do |config|
  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
  end

  config.before(:each) do
    DatabaseCleaner.strategy = :transaction
  end

  config.before(:each, js: true) do
    DatabaseCleaner.strategy = :truncation
  end

  config.before(:each) do
    DatabaseCleaner.start
  end

  config.after(:each) do
    DatabaseCleaner.clean
  end
end

Basically, this says that for js tests, don’t use transactions but truncate the database tables instead.

4. Poltergeist

Poltergeist is a Capybara driver for PhantomJS, a nice tool that lets you automate JS testing without a browser.

This doesn’t take much configuration, but if you want to enable remote debugging (so you can attach a javascript console to your tests), you can use the following helper.

In spec/support/capybara.rb:

require 'capybara/rspec'
require 'capybara/poltergeist'
Capybara.register_driver :poltergeist_debug do |app|
  Capybara::Poltergeist::Driver.new(app, inspector: true, debug: true)
end
Capybara.javascript_driver = :poltergeist_debug
Capybara.default_wait_time = 5

5. Waiting for asynchronous JS events

Capybara gives you many tools to help avoid timing issues, but sometimes they’re still not enough. Capybara 2.0 removed the wait_until feature, so you have to find another way to replicate this feature if Capybara’s built-in waiting isn’t working for you. After reading this post, I adapted their become_truematcher.

In spec/support/wait_steps.rb:

require "timeout"

RSpec::Matchers.define :become_true do
  match do |block|
    begin
      Timeout.timeout(Capybara.default_wait_time) do
        until value = block.call
          sleep 0.1
        end
        value
      end
    rescue TimeoutError
      false
    end
  end

  def supports_block_expectations?
    true
  end
end

This means you can do things like:

expect{page.evaluate_script('someJSFunction()')}.to become_true

and the above code will wait until the JavaScript function returns true (eg. to indicate page readyness).

6. Troubleshooting non-determinism

If you’re still having trouble with non-deterministic tests:

7. The full helper file

This is my spec/rails_helper.rb in full:

# This file is copied to spec/ when you run 'rails generate rspec:install'
ENV["RAILS_ENV"] ||= 'test'
require 'spec_helper'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'

# Add additional requires below this line. Rails is not loaded until this point!
require 'factory_girl_rails'
include Warden::Test::Helpers
Warden.test_mode!

Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }

ActiveRecord::Migration.maintain_test_schema!

RSpec.configure do |config|
  config.use_transactional_fixtures = false

  config.include FactoryGirl::Syntax::Methods
  config.infer_spec_type_from_file_location!

  # Fast login for tests that specify 'login: true'
  config.before(:each, login: true) do
    @user = FactoryGirl.create(:user)
    login_as(@user, scope: :user)
  end

  # Seed the database first
  config.before(:suite) do
    Rails.application.load_seed
  end
end

Hope someone else finds it useful.

Mike

Automated Rails testing with Capybara and PhantomJS

Post navigation


Leave a Reply

Your email address will not be published. Required fields are marked *