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/*
About the Author