Jeff DeanJeff Dean
Writing and running Jasmine specs with Rails 3.1 and Coffeescript
edit Posted by Jeff Dean on Monday July 11, 2011 at 03:19PM

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/*

Comments

  1. Davis W. Frank Davis W. Frank on July 16, 2011 at 02:40PM

    Thanks for this, Jeff. I've just tweeted it out from the Jasmine account.

  2. Justin Searls Justin Searls on July 16, 2011 at 02:54PM

    Were you looking for a setup that would allow you to drive your code with feedback from the command line or the browser?

    Put another way, does the headless CI option offer fast enough feedback to write your code against it (without refreshing a GUI browser)?

  3. Jeff Dean Jeff Dean on July 18, 2011 at 11:02AM

    I only use the headless option for CI, so I don't have to have a full windowed OS on my CI box. Locally I run everything in the browser.

  4. Luis Romero Luis Romero on July 21, 2011 at 03:25PM

    Thanks for posting this!

    I had to tweak the jasmin_config.rb file to clean out the 'spec/javascripts/generated' directory before populating it each time I reloaded my browser. Otherwise I was getting duplicated application*.js files each time I ran my specs. It seems to me this is something the 'assets:clean' task should handle?? - not sure if maybe my setup is off somehow.

    Anyways, aside from that, it's working magically!

  5. Jeff Dean Jeff Dean on July 24, 2011 at 05:51PM

    It does seem like assets:clean should handle that. I'd have to check to see if that was happening for us - we may have been getting duplicate files, but I didn't check for that specifically.

  6. Lee Atchison Lee Atchison on July 31, 2011 at 09:43PM

    Does the headless option suppose to prevent Firefox from actually starting up and running in a window? That's what I thought, but both jasmine:ci and jasmine:ci_headless actually start a browser and hence need a display...

    Am I missing something!

    Thanks for the article! This is very helpful!

  7. Austin Austin on August 01, 2011 at 07:30AM

    @Jeff I am having the same issue with the clean-up

  8. James A. Rosen James A. Rosen on August 04, 2011 at 04:32PM

    As far as I can tell, this solution does not recompile assets on a reload. No matter what files I change and how many times I refresh the page, I never see "Precompiling assets..." in the Jasmine server log and never see the updated assets. Is that beyond the scope of this tip, or am I doing something wrong?

  9. caleb cohoon caleb cohoon on August 07, 2011 at 06:09PM

    So awesome! Thanks for sharing this! :)

  10. Patrick Patrick on August 08, 2011 at 01:54PM

    @Lee I was caught off-guard by that too. Headless blocks are explained here: https://github.com/leonid-shevtsov/headless

  11. Jarvis Jarvis on August 10, 2011 at 05:47PM

    asset:clean removes files in public/assets by default. Duplicate generated assets will accumulate between runs. This works:

    # ::Rake.application['assets:clean'].reenable
    # ::Rake.application['assets:clean'].invoke
    
    assets = Rails.application.config.assets
    rm_rf ::Rails.application.assets.static_root, :secure => true
    
  12. Jeff Dean Jeff Dean on August 16, 2011 at 09:22AM

    @jarvis / @james / @austin / @luis: thanks for reporting the assets:clear bug. I've updated the post to include the a line that removes the entire generated directory, since there was also a bug with specs not being properly cleared out.

    See the remove all generated files in the example above - the whole method now looks like this:

    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
      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
    
  13. Mario A Chavez Mario A Chavez on August 25, 2011 at 04:08PM

    Hello;

    I did try your solution to use jasmine, but I've found that calling invoke on assets:precompile ::Rake.application['assets:precompile'].invoke

    makes jasmine server to fail with: Don't know how to build task 'jasmine'

    It seems that jasmine is trying to restart the server but fail to.

    I did try running "precompile_app_assets" method within the rails console - I did load rake - an when I get to execute ::Rake.application['assets:precompile'].invoke, rails console breaks and gave rails help display.

    I'm running rails 3.1rc6 with ruby-1.9.2p290. At this point I have not found a solution for this.

  14. Jeff Dean Jeff Dean on August 30, 2011 at 08:02AM

    @mario - thanks for the report. We upgraded our app to Rails rc6 and everything broke for us as well. We got stuck in the same place, and currently we have no solution.

    I'll update this post once we find something, but given how many commits are still going into the Rails "release candidates" related to the asset pipeline, I'm not going to waste any time on it until Rails is released (and possible re-released a few times to shake out the bugs).

    It appears there is also a release candidate for the Jasmine gem - I'll report any progress we make here.

  15. David Kahn David Kahn on September 08, 2011 at 05:58PM

    Jeff, thanks for this post. I have the same predicament as Mario Chavez regarding the "dont know how to build task 'jasmine'"... I have gotten around it by writing a rake task which just precompiles the assets to the standard public dir and then copies them to the spec location for jasmine... Just wondering if you all have come up with any better solution as this one is just not fun, and slow. What is strange also is that within my own rake task, if I call 'rake jasmine' at the end, it fails with the same message as when running your code in the jasmine_config.rb.

  16. Anthony Navarre Anthony Navarre on September 14, 2011 at 11:57AM

    For those of you who, like myself, have been trying to get this to work w/ stable Rails 3.1.0, you may have encountered that #static_root= has been deprecated (and subsequently removed).

    The Jasmine gem has a release candidate out that has promising support for direct access to the asset pipeline.

    Here's the reference I used:

    https://groups.google.com/group/jasmine-js/msg/e31c847bc3cb2e2d?pli=1

    and here's what worked in my Gemfile:

    gem 'jasmine', '1.2.0.rc1', git: 'https://github.com/pivotal/jasmine-gem.git', ref: '5a7524ae9eaea4fe106a7aaa90ccfb1bc137abe7'
    
  17. Tester Tester on September 23, 2011 at 01:44PM

    @Jeff Dean Any updates on how this will work with Rails 3.1 stable?

    Thanks

  18. Jeff Dean Jeff Dean on September 26, 2011 at 09:19AM

    It looks like there is some support for the asset pipeline in the Jasmine rc gem (1.1.0.rc4), but I haven't verified.

  19. Kurt Ruppel Kurt Ruppel on October 05, 2011 at 12:07AM

    FWIW, to beat Rails 3.1.0 into submission with Jasmine 1.1.0 and overcome the deprecation of config.static_root, I set config.assets.manifest and config.assets.prefix to point to the compiled files:

    def precompile_app_assets
      puts "Precompiling assets..."
    
      ENV["RAILS_GROUPS"] ||= "assets"
      ENV["RAILS_ENV"]    ||= "test"
    
      # make sure the Rails environment is loaded
      ::Rake.application['environment'].invoke
    
      # Previously, ::Rails.application.assets.static_root was set to
      # a temporary compiled dir. In lieu of sprockets' deprecation of
      # static_root, ::Rails.application.assets.manifest and
      # ::Rails.application.config.assets.prefix are now set to the compiled
      # dir.
      ::Rails.application.config.assets.manifest =
        "spec/javascripts/generated/assets"
      ::Rails.application.config.assets.prefix =
        "../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