John Keith

Hi, I'm a developer that loves Ruby, Rails, Angular, and iOS.

Ajax Testing Using RSpec, Capybara, and Puffing Billy

Launch Academy has come to an end. It was an incredible 10 weeks, but I’m already itching to jump into a new project while beginning my developer job search.

Ajax testing was one of the areas I only briefly touched on at the end of the course so I wanted to spend some time this week exploring the ins and outs of this crucial aspect of testing. The engineers at Launch said that Ajax and other key Javascript operations of an app should be tested using a JavaScript testing framework. I’m still tempted, however, see benefits to testing interactions that rely on Ajax with integration tests, especially if they are fundamental to a user’s experience on the page. The example below is how I implemented a couple of Ajax tests using the GitHub API and a simple form.

To start with, it is important to note that testing JavaScript requires you to add some extra configuration in your rails_helper and in your specs. Capybara by default uses the Selenium javascript driver to test Javascript on the page. From my initial testing, it seems like Selenium returns consistent results, but takes more time than other methods. Selenium opens a new instance of the browser for each test, which on a big test suite I imagine could lead to considerable delay.

To enable Javascript on a test, simply pass the js: true option to the test. (Or better yet, pass js: true to your entire feature to use Javascript on all the tests in a spec). When you run RSpec, you should see browser windows pop up and replay the actions mapped out in your integration tests.

There are other Javascript drivers, Poltergeist being the second one I implemented. Poltergeist needs a little extra setup to get it running, as it is built upon PhantomJS, which allows it to run your tests without opening new browser windows each time.

First, add Poltergeist to your Gemfile.

1
gem 'poltergeist'

Then, in your rails_helper, specify the default javascript driver to be used in your tests.

1
Capybara.javascript_driver = :poltergeist

Again, make sure either a single test or your entire feature has Javascript enabled by passing the js: true option.

1
2
3
require_relative '../rails_helper'

feature "user enters basic information on homepage", js: true do

Finally, install PhantomJS from the link above. When running RSpec, you should see your test suite function as normal, without browser windows materializing all over the screen.

One major issue that arises with these two approaches is API calls. When using these Javascript drivers on their own, the test suite is still reaching out and making API requests. At first I thought I was safe – I had VCR and WebMock enabled in order to record and replay HTTP interactions. I found out the old fashioned way – by disconnecting from my wi-fi and causing the tests to fail – that these mechanisms were in fact not capturing my Ajax requests.

To grab these Ajax requests, I hunted down a gem called Puffing Billy. Puffing Billy handles the recording of Ajax calls like VCR, allowing you to run the test suite with genuine API requests once and then subsequently replay the recordings made from the first calls. Puffing Billy has a great readme on its GitHub page, but below are the steps I followed to get it working.

First, add Puffing Billy to your Gemfile.

1
gem 'puffing-billy'

Then, require Puffing Billy in your rails_helper.

1
require 'billy/rspec'

Next, set your default Javascript driver in your rails_helper. (Puffing Billy supports Selenium, Poltergeist, and Webkit. See the docs for more details.)

1
Capybara.javascript_driver = :poltergeist_billy

Lastly, configure Puffing Billy to cache Ajax interactions with this configure block in your rails_helper.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Billy.configure do |c|
  c.cache = true
  c.cache_request_headers = false
  c.ignore_params = ["http://www.google-analytics.com/__utm.gif",
                     "https://r.twimg.com/jot",
                     "http://p.twitter.com/t.gif",
                     "http://p.twitter.com/f.gif",
                     "http://www.facebook.com/plugins/like.php",
                     "https://www.facebook.com/dialog/oauth",
                     "http://cdn.api.twitter.com/1/urls/count.json"]
  c.path_blacklist = []
  c.persist_cache = true
  c.ignore_cache_port = true # defaults to true
  c.non_successful_cache_disabled = false
  c.non_successful_error_level = :warn
  c.non_whitelisted_requests_disabled = false
  c.cache_path = 'spec/req_cache/'
end

That should be it! Now you can run your test suite, watch it pass, and see a folder of the requests in req_cache.

Below is an example of a basic feature I wrote for what I believe is the world’s first, only, and hopefully last GitHub dating app. (Not sure I’ll be continuing with that side project…but it was a good example for learning Ajax testing). I’ll also include all the necessary configs you need to get RSpec, Capybara, and Puffing Billy to run these test.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# onboarding page feature

require_relative '../rails_helper'

feature "user enters basic information on homepage", js: true do
  scenario "fills in github username" do
    visit root_path
    fill_in "Your Github username", with: "johnkeith"
    select "Male", from: "gender-select"
    expect(page).to have_xpath "//img[@src=\"https://avatars.githubusercontent.com/u/4976905?\"]"
    expect(page).to have_css "div.has-success"
  end

  scenario "fills in a github username that doesn't exist" do
    visit root_path
    fill_in "Your Github username", with: "notaghuser"
    select "Male", from: "gender-select"
    expect(page).to have_content "Sorry, that is not a Github username."
    expect(page).to have_css "div.has-error"
  end

  scenario "selects own gender" do
    visit root_path
    select "Male", from: "gender-select"
    fill_in "Your Github username", with: "johnkeith"
    expect(page).to have_css "div.has-success"
  end

  scenario "selects preference for matches" do
    visit root_path
    select "Men", from: "match-pref-select"
    fill_in "Your Github username", with: "johnkeith"
    expect(page).to have_css "div.has-success"
  end

  scenario "fills username, selects gender, selects preferences" do
    visit root_path
    fill_in "Your Github username", with: "johnkeith"
    select "Male", from: "gender-select"
    select "Women", from: "match-pref-select"
    expect(page).to have_xpath "//img[@src=\"https://avatars.githubusercontent.com/u/4976905?\"]"
    expect(page).to have_select("gender-select", selected: "Male")
    expect(page).to have_select("match-pref-select", selected: "Women")
  end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# rails_helper.rb

ENV["RAILS_ENV"] ||= 'test'
require 'spec_helper'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'capybara/rails'
require 'shoulda/matchers'
require 'capybara/poltergeist'
require 'billy/rspec'

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

ActiveRecord::Migration.maintain_test_schema!

Capybara.javascript_driver = :poltergeist_billy

Billy.configure do |c|
  c.cache = true
  c.cache_request_headers = false
  c.ignore_params = ["http://www.google-analytics.com/__utm.gif",
                     "https://r.twimg.com/jot",
                     "http://p.twitter.com/t.gif",
                     "http://p.twitter.com/f.gif",
                     "http://www.facebook.com/plugins/like.php",
                     "https://www.facebook.com/dialog/oauth",
                     "http://cdn.api.twitter.com/1/urls/count.json"]
  c.path_blacklist = []
  c.persist_cache = true
  c.ignore_cache_port = true # defaults to true
  c.non_successful_cache_disabled = false
  c.non_successful_error_level = :warn
  c.non_whitelisted_requests_disabled = false
  c.cache_path = 'spec/req_cache/'
end
1
2
3
4
5
6
7
8
9
# spec_helper.rb

require 'webmock/rspec'

WebMock.disable_net_connect!(allow_localhost: true)

RSpec.configure do |config|

end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Gemfile

group :development, :test do
  gem 'dotenv-rails'
  gem 'rspec-rails'
  gem 'capybara'
  gem 'launchy'
  gem 'factory_girl_rails'
  gem 'pry-rails'
  gem 'poltergeist'
  gem 'puffing-billy'
  gem 'webmock'
  gem 'valid_attribute'
  gem 'shoulda-matchers'
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/ home page with form to be tested

.row
  .col-md-3
    .portrait-placeholder
      %img{ src: "", class: "img-responsive img-thumbnail", id: "github-avatar" }
  .col-md-9
    %form{ role: "form", id: "onboard-form" }
      .form-group.form-group-lg#github-username-group
        %input{ type: "text", class: "form-control input-lg", placeholder: "Your Github username", id: "github-username" }
      .form-group.form-group-lg#gender-group
        %select{ class: "form-control input-lg", id: "gender-select" }
          %option{ value: "", "disabled" => "disabled", "selected" => "selected" } Select your gender
          %option{ value: "male", id: "gender-male" } Male
          %option{ value: "female", id: "gender-female" } Female
      .form-group.form-group-lg#match-group
        %select{ class: "form-control input-lg", id: "match-pref-select" }
          %option{ value: "", "disabled" => "disabled", "selected" => "selected" } Select your preference
          %option{ value: "men", id: "match-men" } Men
          %option{ value: "women", id: "match-women" } Women
    .form-errors
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// jquery for form 

$(function(){
  $("#github-username").change(function(){
    var username = $("#github-username").val();
    $.ajax({
      type: "GET",
      url: "https://api.github.com/users/" + username,
      success: function(d) {
        $("#github-avatar").attr("src", d.avatar_url);
        $("#github-username-group").addClass("has-success");
        $("#github-username-group").removeClass("has-error");
        $(".form-errors").text("");

      },
      error: function() {
        $(".form-errors").text("Sorry, that is not a Github username.");
        $("#github-avatar").attr("src", "");
        $("#github-username-group").addClass("has-error");
        $("#github-username-group").removeClass("has-success");
      }
    });
  });
  $("#gender-select").change(function(){
    $("#gender-group").addClass("has-success");
  });
  $("#match-pref-select").change(function(){
    $("#match-group").addClass("has-success");
  });
});

Comments