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:
- https://github.com/bradphelan/jasminerice
- https://github.com/jnicklas/evergreen/issues/19
- http://jashkenas.github.com/coffee-script/
- https://github.com/pivotal/jasmine/wiki
- http://groups.google.com/group/jasmine-js/browse_thread/thread/c9c30854ecfd915d
- https://github.com/superchris/backbone_coffeescript_demo
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/*
Thanks for this, Jeff. I’ve just tweeted it out from the Jasmine account.
July 16, 2011 at 2:40 pm
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)?
July 16, 2011 at 2:54 pm
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.
July 18, 2011 at 11:02 am
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!
July 21, 2011 at 3:25 pm
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.
July 24, 2011 at 5:51 pm
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!
July 31, 2011 at 9:43 pm
@Jeff I am having the same issue with the clean-up
August 1, 2011 at 7:30 am
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?
August 4, 2011 at 4:32 pm
So awesome! Thanks for sharing this! :)
August 7, 2011 at 6:09 pm
@Lee
I was caught off-guard by that too. Headless blocks are explained here:
https://github.com/leonid-shevtsov/headless
August 8, 2011 at 1:54 pm
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
August 10, 2011 at 5:47 pm
@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
August 16, 2011 at 9:22 am
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.
August 25, 2011 at 4:08 pm
@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.
August 30, 2011 at 8:02 am
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.
September 8, 2011 at 5:58 pm
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′
September 14, 2011 at 11:57 am
@Jeff Dean Any updates on how this will work with Rails 3.1 stable?
Thanks
September 23, 2011 at 1:44 pm
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.
September 26, 2011 at 9:19 am
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
October 5, 2011 at 12:07 am