Writing and running Jasmine specs with Rails 3.1 and Coffeescript

July 11, 2011 Pivotal Labs

In this post I will describe one way to write Jasmine tests in Coffeescript, and test javascript files written in Coffeescript that are served as part of Rails 3.1’s asset pipeline.

UPDATE – as of Rails 3.1.rc8 nothing in this post works at all. Once I figure it out I’ll post the results here.

I recently started a rails 3.1 project along with fellow pivot Charles LeRose. We decided to try out the Rails 3.1 release candidate, which supports the “asset pipeline”, in which you can write javascript in Coffeescript. We wanted to test this javascript with Jasmine, and write our tests in Coffeescript as well.

To start, we looked into several existing solutions, including the following:

After looking through them, we decided:

  • we didn’t want to add a route to our app just to run tests
  • we didn’t want to add Barista just for Jasmine (since we already had the ‘blessed’ asset pipeline)
  • we didn’t want to have to run our Rails app in order to run jasmine tests, since setting that up on CI is a pain.

So we decided to roll our own. At a high level, we hooked into the Jasmine::Config class to clear and regenerate the rails assets into a tmp directory, compile the coffee script specs into a tmp directory, then run jasmine off of the compiled files.

Install jasmine

# Gemfile
group :development, :test do
  gem 'jasmine', '1.0.2.1'
  gem 'headless', '0.1.0'
end

Then execute:

jasmine init

At this point you can delete the generated js example files and specs.

Write a spec in coffeescript

The first piece of javascript we wanted to put in our app was the excellent Less Client Logic snippet, so we wrote a simple spec for it:

# spec/javascripts/coffee/remote_content_spec.js.coffee
describe "ajax updates", ->
  it "should update my content", ->
    $('#jasmine_content').html("<div data-content-key='foo'>x</div>")
    $('#jasmine_content').append("<a data-remote-content='true' id='mylink'>Some link</a>")
    $('#mylink').trigger("ajax:success", ["<div data-content-key='foo'>y</div>"])
    expect($("#jasmine_content div[data-content-key]").html()).toEqual("y")

We decided to put all of our coffee scripts into spec/javascripts/coffee, but there’s nothing magical about that path.

In order to get the jasmine specs to run, we needed to compile the coffee script into javascript, then tell jasmine where the compiled files were.

Jasmine asks the Jasmine::Config for it’s list of javascript files, so that seemed like an excellent place to start.

The Rails internals are likely to change, so we decided to only use the high-level rake tasks provided.

# spec/javascripts/support/jasmine_config.rb
# when jasmine starts the server out-of-process, it needs this in order to be able to invoke the asset tasks
unless Object.const_defined?(:Rake)
  require 'rake'
  load File.expand_path('../../../../Rakefile', __FILE__)
end

module Jasmine
  class Config

    def js_files(spec_filter = nil)
      # remove all generated files
      generated_files_directory = File.expand_path("../../generated", __FILE__)
      rm_rf generated_files_directory, :secure => true

      precompile_app_assets
      compile_jasmine_javascripts

      # this is code from the original jasmine config js_files method - you could also just alias_method_chain it
      spec_files_to_include = spec_filter.nil? ? spec_files : match_files(spec_dir, [spec_filter])
      src_files.collect {|f| "/" + f } + helpers.collect {|f| File.join(spec_path, f) } + spec_files_to_include.collect {|f| File.join(spec_path, f) }
    end

    private

    # this method compiles all the same javascript files your app will
    def precompile_app_assets
      puts "Precompiling assets..."

      # make sure the Rails environment is loaded
      ::Rake.application['environment'].invoke

      # temporarily set the static assets location from public/assets to our spec directory
      ::Rails.application.assets.static_root = Rails.root.join("spec/javascripts/generated/assets")

      # rake won't let you run the same task twice in the same process without re-enabling it

      # once the assets have been cleared, recompile them into the spec directory
      ::Rake.application['assets:precompile'].reenable
      ::Rake.application['assets:precompile'].invoke
    end

    # this method compiles all of the spec files into js files that jasmine can run
    def compile_jasmine_javascripts
      puts "Compiling jasmine coffee scripts into javascript..."
      root = File.expand_path("../../../../spec/javascripts/coffee", __FILE__)
      destination_dir = File.expand_path("../../generated/specs", __FILE__)

      glob = File.expand_path("**/*.js.coffee", root)

      Dir.glob(glob).each do |srcfile|
        srcfile = Pathname.new(srcfile)
        destfile = srcfile.sub(root, destination_dir).sub(".coffee", "")
        FileUtils.mkdir_p(destfile.dirname)
        File.open(destfile, "w") {|f| f.write(CoffeeScript.compile(File.new(srcfile)))}
      end
    end

  end
end

#...

Once the config class has the appropriate methods, we need to tell jasmine where to find the javascript files:

# reference the compiled production javascript file
# we need the asterisk because the generated file is named something like application-123482746352.js
src_files:
  - spec/javascripts/generated/assets/application*.js

# this directive (the default) finds all spec files in all subdirectories, so no need to change it
spec_files:
  - '**/*[sS]pec.js'

Make it pass

Making that spec pass is pretty simple, and looks something like this:

# app/assets/javascripts/remote_content.js.coffee
updateContent = (event, newContent) ->
  $(newContent).filter('[data-content-key]').each ->
    contentKey = $(this).attr("data-content-key")
    $("[data-content-key=" + contentKey + "]").html($(this).html())

$('[data-remote-content]').live 'ajax:success', (e, data, status, request) ->
  updateContent(e, data)

Make it run headlessly in CI

To get the specs to run headlessly, we added the following task (thanks to Mike Gehard for the pointers):

# lib/tasks/headless_jasmine.rake
namespace :jasmine do
  namespace :ci do
    desc "Run Jasmine CI build headlessly"
    task :headless do
      Headless.ly do
        puts "Running Jasmine Headlessly"
        Rake::Task['jasmine:ci'].invoke
      end
    end
  end
end

And we added the following to our build script:

# build.sh
bundle exec rake jasmine:ci:headless

Git ignore the generated files

One final step before committing was to ignore those generated files:

# .gitignore
spec/javascripts/generated/*

About the Author

Biography

Previous
Pivotal Tracker for iOS update now in the App Store
Pivotal Tracker for iOS update now in the App Store

An update of Pivotal Tracker for iOS (released last week )is now available in the iTunes App Store. It incl...

Next
New in Pivotal Tracker: Auto-suggest for labels, improved Google sign-in
New in Pivotal Tracker: Auto-suggest for labels, improved Google sign-in

In this week's update to Pivotal Tracker, we've made it easier to apply and remove labels to/from stories, ...