Automated Rails testing with Capybara and PhantomJS

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

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)

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

<div class="highlight"><pre><span class="n">describe</span> <span class="err">'My</span> <span class="n">test',</span> <span class="n">login:</span> <span class="nb">true,</span> <span class="n">js:</span> <span class="nb">true</span> <span class="k">do</span>

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:

<div class="highlight"><pre><span class="no">RSpec.configure</span> <span class="k">do</span> <span class="o">|config|</span>
  <span class="n">config.before(:suite)</span> <span class="k">do</span>
    <span class="no">DatabaseCleaner.clean_with(:truncation)</span>
  <span class="k">end</span>

  <span class="n">config.before(:each)</span> <span class="k">do</span>
    <span class="no">DatabaseCleaner.strategy</span> <span class="o">=</span> <span class="ss">:transaction</span>
  <span class="k">end</span>

  <span class="n">config.before(:each,</span> <span class="ss">js:</span> <span class="kp">true)</span> <span class="k">do</span>
    <span class="no">DatabaseCleaner.strategy</span> <span class="o">=</span> <span class="ss">:truncation</span>
  <span class="k">end</span>

  <span class="n">config.before(:each)</span> <span class="k">do</span>
    <span class="no">DatabaseCleaner.start</span>
  <span class="k">end</span>

  <span class="n">config.after(:each)</span> <span class="k">do</span>
    <span class="no">DatabaseCleaner.clean</span>
  <span class="k">end</span>
<span class="k">end</span></pre></div>

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:

<div class="highlight"><pre><span class="nb">require</span> <span class="s1">'capybara/rspec'</span>
<span class="nb">require</span> <span class="s1">'capybara/poltergeist'</span>
<span class="no">Capybara.register_driver</span> <span class="ss">:poltergeist_debug</span> <span class="k">do</span> <span class="o">|app|</span>
  <span class="ss">,</span> <span class="ss">inspector:</span> <span class="kp">true,</span> <span class="ss">debug:</span> <span class="kp">true)</span>
<span class="k">end</span>
<span class="no">Capybara.javascript_driver</span> <span class="o">=</span> <span class="ss">:poltergeist_debug</span>
<span class="no">Capybara.default_wait_time</span> <span class="o">=</span> <span class="mi">5</span></pre></div>

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:

<div class="highlight"><pre><span class="nb">require</span> <span class="s2">"timeout"</span>

<span class="ss">RSpec::Matchers.define</span> <span class="ss">:become_true</span> <span class="k">do</span>
  <span class="n">match</span> <span class="k">do</span> <span class="o">|block|</span>
    <span class="k">begin</span>
      <span class="no">Timeout.timeout(Capybara.default_wait_time)</span> <span class="k">do</span>
        <span class="k">until</span> <span class="n">value</span> <span class="o">=</span> <span class="n"></span>
          <span class="nb">sleep</span> <span class="mi">0.1</span>
        <span class="k">end</span>
        <span class="n">value</span>
      <span class="k">end</span>
    <span class="k">rescue</span> <span class="no">TimeoutError</span>
      <span class="kp">false</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">supports_block_expectations?</span>
    <span class="kp">true</span>
  <span class="k">end</span>
<span class="k">end</span>

This means you can do things like:

<div class="highlight"><pre><span class="n">expect{page.evaluate_script('someJSFunction()')}.to</span> <span class="n">become_true</span>

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:

<div class="highlight"><pre><span class="c1"># This file is copied to spec/ when you run 'rails generate rspec:install'</span>
<span class="no">ENV["RAILS_ENV"]</span> <span class="o">||=</span> <span class="s1">'test'</span>
<span class="nb">require</span> <span class="s1">'spec_helper'</span>
<span class="nb">require</span> <span class="no">File.expand_path("../../config/environment",</span> <span class="bp">__FILE__)</span>
<span class="nb">require</span> <span class="s1">'rspec/rails'</span>

<span class="c1"># Add additional requires below this line. Rails is not loaded until this point!</span>
<span class="nb">require</span> <span class="s1">'factory_girl_rails'</span>
<span class="kp">include</span> <span class="ss">Warden::Test::Helpers</span>
<span class="no">Warden.test_mode!</span>

<span class="no">Dir[Rails.root.join("spec/support/**/*.rb")].each</span> <span class="p">{</span> <span class="o">|f|</span> <span class="nb">require</span> <span class="n">f</span> <span class="p">}</span>

<span class="ss">ActiveRecord::Migration.maintain_test_schema!</span>

<span class="no">RSpec.configure</span> <span class="k">do</span> <span class="o">|config|</span>
  <span class="n">config.use_transactional_fixtures</span> <span class="o">=</span> <span class="kp">false</span>

  <span class="n">config.include</span> <span class="ss">FactoryGirl::Syntax::Methods</span>
  <span class="n">config.infer_spec_type_from_file_location!</span>

  <span class="c1"># Fast login for tests that specify 'login: true'</span>
  <span class="n">config.before(:each,</span> <span class="ss">login:</span> <span class="kp">true)</span> <span class="k">do</span>
    <span class="vi">@user</span> <span class="o">=</span> <span class="no">FactoryGirl.create(:user)</span>
    <span class="n">login_as(@user,</span> <span class="ss">scope:</span> <span class="ss">:user)</span>
  <span class="k">end</span>

  <span class="c1"># Seed the database first</span>
  <span class="n">config.before(:suite)</span> <span class="k">do</span>
    <span class="no">Rails.application.load_seed</span>
  <span class="k">end</span>
<span class="k">end</span>

Hope someone else finds it useful.


Leave a comment

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