GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS

Written by thoughtbot

JavaScript Integration Testing Example: Installing And Using Mixpanel

We’ve been on a quest for years to make sure our integration tests cover the JavaScript components of the app. Here’s an example: installing Mixpanel in Copycopter to track visits, user sign ups, user activity during the free trial, and subscriptions.

Gemfile:

gem 'capybara-webkit'

features/support/env.rb:

Capybara.javascript_driver = :webkit

features/mixpanel.feature:

Background:
  Given the following plan exists:
    | id | name       | price | trial |
    | 16 | Supersonic | 5     | false |
    | 5  | Trial      | 0     | true  |
  And the following limits exist:
    | plan             | name     | value |
    | name: Supersonic | users    | 13    |
    | name: Supersonic | projects | 14    |

@javascript
Scenario: Track visitor learning about Copycopter
  When I go to the homepage
  Then mixpanel should track the "visited-home" event
  When I follow "Take a tour"
  Then mixpanel should track the "clicked-tour" event
  When I follow "Next, view plans and pricing"
  Then mixpanel should track the "clicked-next-to-plans-and-pricing" event

@javascript
Scenario: Track visitor signing up for free trial
  When I go to the homepage
  And I follow "Plans and Pricing"
  Then mixpanel should track the "clicked-plans-and-pricing" event
  When I follow "Choose free trial"
  Then mixpanel should track the "viewed-plan" event with the properties:
    | plan_id | 5 |

We use the @javascript tag, which will use Capybara Webkit to drive the browser in these tests.

We explicitly set the id’s of the ActiveRecord objects so we can check that Mixpanel receives the right plan id’s using their properties feature.

features/step_definitions/mixpanel_steps.rb:

Then %r{^mixpanel should track the "(.*)" event$} do |event_name|
  mpq = JSON.parse(evaluate_script(%{JSON.stringify(mpq);}))
  mpq.should include(["track", event_name])
end

Then %r{^mixpanel should track the "(.*)" event with the properties:$} do |event_name, table|
  mpq        = JSON.parse(evaluate_script(%{JSON.stringify(mpq);}))
  properties = table.transpose.hashes.first
  mpq.should include(["track", event_name, properties])
end

This is a little funky. We’re using JSON.stringify via json2.js and then Ruby’s JSON.parse to convert Mixpanel’s mpq Javascript object into its Ruby equivalent in order to invoke expectations on it.

Therefore, we need to include json2.js in our app:

curl https://github.com/douglascrockford/JSON-js/raw/master/json2.js > public/javascripts/json2.js

app/views/shared/_javascript.html.erb:

<% if Rails.env.test? %>
  <%= javascript_include_tag "json2" %>
<% end %>

That smells like a hack, but whatever…

Also in that partial, the actual setup for Mixpanel:

<script type="text/javascript">
  var mpq = [];
  <% if Rails.env.staging? || Rails.env.production? -%>
    mpq.push(["init", "<%= MIXPANEL_TOKEN %>"]);
    (function() {
      var mp = document.createElement("script"); mp.type = "text/javascript"; mp.async = true;
      mp.src = (document.location.protocol == 'https:' ? 'https:' : 'http:') + "//api.mixpanel.com/site_media/js/api/mixpanel.js";
      var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(mp, s);
    })();
  <% end -%>
</script>

That mpq object looks familiar. We’re testing against it in our integration suite. It’s just a JavaScript Array.

We only include the rest of the Mixpanel setup in staging and production. It stuffs mixpanel.js into the DOM asynchronously.

We interpolate our Mixpanel account’s token based on the environment so we can run acceptance on our user story on staging.

config/environments/staging.rb:

MIXPANEL_TOKEN = "our-staging-token".freeze

config/environments/production.rb:

MIXPANEL_TOKEN = "our-production-token".freeze

To get the rest of the integration test passing, we follow the Mixpanel API normally.

views/homes/show.html.erb:

$(function () {
  mpq.push(["track", "visited-home"]);
  $("#tour-cloud").click(function () {
    mpq.push(["track", "clicked-tour"]);
  });
  $("#plans-cloud").click(function () {
    mpq.push(["track", "clicked-plans-and-pricing"]);
  });
  $("#next-to-plans").click(function () {
    mpq.push(["track", "clicked-next-to-plans-and-pricing"]);
  });
});

views/accounts/new.html.erb:

$(function () {
  mpq.push(["track", "viewed-plan", { plan_id: "<%= @plan.id %>" }]);
});

This use case is relatively common. Include some external service’s JavaScript and use their Javascript API in order to get good analytics on the app.

To make it happen smoothly, there’s a lot of interpolation and Ruby mixing with HTML and JavaScript. Things could go wrong and it feels good to have integration coverage for it.