Using QUnit with CoffeeScript
QUnit is a unit testing framework for JavaScript, from the jQuery project. Earlier this week I wrote about how I chose it over Jasmine for my current project. This article explains how I setup QUnit to test my CoffeeScript, along with the Sinon mocking/stubbing library.
Downloading QUnit
Let's assume that you want to keep your tests in a folder called
test/javascripts
(relative to your project's directory), and that the
CoffeeScript code that you want to test lives in a folder called
app/assets/javascripts
. This is where the files live in my Rails app; if
you're not using Rails just substitute the appropriate paths for your project
in the code that follows.
Start by making a directory to store QUnit itself, and then download qunit.js
and qunit.css
:
$ mkdir -p test/javascripts/qunit
$ cd test/javascripts/qunit
$ curl -O https://raw.github.com/jquery/qunit/master/qunit/qunit.js
$ curl -O https://raw.github.com/jquery/qunit/master/qunit/qunit.css
We'll also want the in-browser CoffeeScript compiler so that we can compile
CoffeeScript within text/coffeescript
script tags on the fly. I've stored it
in test/javascripts
:
$ cd ../
$ curl -O http://jashkenas.github.com/coffee-script/extras/coffee-script.js
The test suite HTML file
QUnit needs an HTML file that will load QUnit, our CoffeeScript, and our tests
(which are also be written in CoffeeScript). The HTML file needs a few
placeholder elements in it that qunit.js
uses when rendering the results of
your test suite.
The onus is on you to set this file up, but there are plenty of examples
online. Here's mine, which I saved in test/javascripts/index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>QUnit Test Suite</title>
<link rel="stylesheet" href="qunit/qunit.css" type="text/css" media="screen">
</head>
<body>
<h1 id="qunit-header">QUnit Tests</h1>
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<div id="qunit-fixture">
</div>
<script type="text/javascript" src="qunit/qunit.js"></script>
<script type="text/javascript" src="coffee-script.js"></script>
<script type="text/coffeescript">
</script>
</body>
</html>
You'll notice that I've loaded qunit.js
and coffee-script.js
.
All we need to do now is to load our tests inside the empty
text/coffeescript
script tag. To check whether everything is setup
properly, let's add a couple of simple tests:
<script type="text/coffeescript">
test 'true should be truthy', ->
ok(true, 'true is not truthy!')
test 'should be able to append to an array', ->
# this test will fail, as this *isn't* how to append to an array
equal([1, 2], [1] + 2)
</script>
Running your tests
Just open the web page in your browser. We've used relative paths for
everything in index.html
, so you can load it directly from the file system,
with a file:///...
URL.
On a Mac? Do this:
$ open test/javascripts/index.html
It should look like this:
Loading tests from external files
You could keep all your tests in index.html
, but I prefer to store my code in
separate files with a .coffee
extension and to just use index.html
as a way
to kick everything off.
I've put this code in my script tag:
<script type="text/coffeescript">
for file in ['models', 'controllers']
lib = "../../app/assets/javascripts/#{file}.js.coffee"
load_test = ->
test = "#{file}_test.coffee"
-> CoffeeScript.load(test)
CoffeeScript.load lib, load_test()
</script>
Let's break that down a bit...
I've used a simple naming convention for my test files. Code in
app/assets/javascripts/foo.js.coffee
is tested by code that lives in
test/javascripts/foo_test.coffee
. The for loop is just loading each of my
modules, and then (via the callback returned by load_test
) loading the
corresponding tests. That's all you need to do; QUnit will do the rest.
The CoffeeScript.load
function takes a URL and a callback to run after load.
I'm loading the tests in a callback to guarantee that the code under test has
been loaded before the test code itself is loaded. If you don't do this you can
see some test failures when your tests run before the code they're testing gets
loaded.
If you're wondering why we need the load_test
function (rather than just
passing an anonymous function as our callback), it's currying the value of the
test
variable. If you don't curry it the current value of file
is used when
the callback executes, rather than the value that it was set to when the
callback is defined. So if you don't curry it, most of your test files won't
get loaded.
Loading third party libraries
I use MooTools and Serenade.js in my project, so I've added these lines to the list of script tags:
<script type="text/javascript" src="../../vendor/assets/javascripts/mootools-core-1.4.4.js"></script>
<script type="text/javascript" src="../../vendor/assets/javascripts/serenade.js"></script>
You can obviously load jQuery, YUI, etc. in the same way.
Adding a stubbing/mocking library
Unit testing gets painful when you can't stub or mock the objects that interact with the code that's under test. I didn't spend a lot of time investigating the options, but as I read the Sinon docs I took quite a shine to it. I noticed that Serande.js uses Sinon, and that's a good enough recommendation for me. There's also a QUnit plugin which makes it "just work".
To install Sinon download both sinon-qunit and Sinon.JS from the sinon-qunit
page and put them in your test/javascripts/qunit
folder.
Then add these tags to index.html
(but make sure you update the version
numbers):
<script type="text/javascript" src="qunit/sinon-1.3.2.js"></script>
<script type="text/javascript" src="qunit/sinon-qunit-1.0.0.js"></script>
Re-running the tests automatically
So that's all fairly simple, but to run the tests you have to switch to your
browser and reload the page. That's a bit painful; wouldn't it be good if your
test suite ran immediately when you save a .coffee
file? I'm now using
guard-livereload
to reload the page in the browser on save.
I think we've dealt with enough for one article, so I'll show you how to set
guard-livereload
up for QUnit (with Rails) in my next post...