Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

LABS
Writing and running Jasmine specs with Rails 3.1 and Coffeescript

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 says:

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

  2. 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 says:

    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 says:

    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 says:

    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 says:

    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 says:

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

  8. James A. Rosen says:

    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 says:

    So awesome! Thanks for sharing this! :)

  10. Patrick says:

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

  11. Jarvis says:

    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 says:

    @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 says:

    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 says:

    @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 says:

    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. 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 says:

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

    Thanks

  18. Jeff Dean says:

    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 says:

    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

Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *