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.