JavaScript BDD, with Jasmine, without a browser

I've been test driving the domain of my build radiator, XFD with the lovely Jasmine BDD framework for JavaScript. Jasmine is lovely. Browsers aren't. Spawning a new browser to run your tests has issues for me:

  • Spawning a browser takes time and ruins my flow,
  • I'm trying to drive out logic - having a browser present will lead my design to un-natural couplings, and
  • It makes Continuous Integration that much harder

So I started investigating what I could do with Rhino and Envjs to make testing with Jasmine more awesome. Ingvald Skaug had been there before. It took me some time to really understand how the pieces fit together works, so I thought I'd expand on it.

Step 1: Check that Jasmine is working

I would have saved so much time if I'd started with this bit. What you need to do is download the core of Jasmine and stick it in your project. I started with the Jasmine RubyGem that spawns a browser and does the plumbing, but for this it's back to basics. In my project it's checked in at lib/jasmine-1.0.1. You need an HTML file to reference all the scripts and kick off the tests. Here's an example derived from the Jasmine docs:


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
  "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <title>Jasmine Test Runner</title>
  <link rel="stylesheet" type="text/css" href="lib/jasmine-1.0.1/jasmine.css"></link>
  <script type="text/javascript" src="lib/jasmine-1.0.1/jasmine.js"></script>
  <script type="text/javascript" src="lib/jasmine-1.0.1/jasmine-html.js"></script>
 
  <!-- include source files here... -->
  <script type="text/javascript" src="src/Player.js"></script>
  <script type="text/javascript" src="src/Song.js"></script>
  
  <!-- include spec files here... -->
  <script type="text/javascript" src="spec/SpecHelper.js"></script>
  <script type="text/javascript" src="spec/PlayerSpec.js"></script>

</head>
<body>
  
<script type="text/javascript">
  jasmine.getEnv().addReporter(new jasmine.TrivialReporter());
  jasmine.getEnv().execute();
</script>

</body>
</html>

In my real project I generate this file at build time from an ERB template, to make sure I get all the source files and tests. However you do it, make sure it works in a browser first. Yes. Really.

Step 2: Get the bits that you need

In my lib directory I have:

  • js.jar - which is the Rhino implementation of JavaScript. I already used this to run JsLint as part of my build
  • env.rhino.1.2.js - which is Envjs - a DOM implementation written in JavaScript.
  • jasmine.console_reporter.js, jasmine.junit_reporter.js and envjs.bootstrap.js - all from Larry Myers' excellent Jasmine Reporters project. Jasmine Reporters is really what glues everything together.

Step 3: Wire up Jasmine Reporters

You can have many Jasmine reporters wired up in the SpecRunner.html. In this example I'm leaving two in - the TrivialReporter that gives HTML/CSS reports, and the ConsoleReporter, which we'll use later. Here's the edit to the SpecRunner file now:


<script type="text/javascript">
  jasmine.getEnv().addReporter(new jasmine.ConsoleReporter());
  jasmine.getEnv().addReporter(new jasmine.TrivialReporter());
  jasmine.getEnv().execute();
</script>

Step 4: Put it all together

Here's where it all happens. In my example I use a shell script but in real life the Rakefile that generates the SpecRunner file also fires up the JVM and checks STDOUT for error messages.

#!/bin/bash
java -jar lib/js.jar -opt -1 lib/envjs.bootstrap.js SpecRunner.html

envjs.bootstrap.js is worth examining, too:

load('lib/env.rhino.1.2.js');

Envjs.scriptTypes['text/javascript'] = true;

var specFile;

for (i = 0; i < arguments.length; i++) {
    specFile = arguments[i];
    
    console.log("Loading: " + specFile);
    
    window.location = specFile
}

This file takes the list of HTML files that you give it and tells the fake browser inside the JVM to load each one. Jasmine then fires and runs your tests:

jsimpson@curie:~/Documents/workspace/jasmine-rhino-envjs$ ./jasmine 
[  Envjs/1.6 (Rhino; U; Linux i386 2.6.32-26-generic; en-US; rv:1.7.0.rc2) Resig/20070309 PilotFish/1.2.13  ]
Loading: SpecRunner.html
Runner Started.
Player : should be able to play a Song ... 
>> Jasmine Running Player should be able to play a Song...
Passed.
when song has been paused : should indicate that the song is currently paused ... 
>> Jasmine Running when song has been paused should indicate that the song is currently paused...
Passed.
when song has been paused : should be possible to resume ... 
>> Jasmine Running when song has been paused should be possible to resume...
Passed.
when song has been paused: 4 of 4 passed.
Player : tells the current song if the user has made it a favorite ... 
>> Jasmine Running Player tells the current song if the user has made it a favorite...
Passed.
#resume : should throw an exception if song is already playing ... 
>> Jasmine Running #resume should throw an exception if song is already playing...
Passed.
#resume: 1 of 1 passed.
Player: 8 of 8 passed.
Runner Finished.

There's also a JUnit compatible XML reporter, courtesy of Larry. This lets you make the Continuous Integration server report test results as usual.

Summary
I'm very impressed. All of my tests that used to run in the browser run headless, with some fiddling of paths. I'm using the Jasmine JQuery plugin, which probably saved my bacon on the test that is too tightly coupled to views. I've collected the example on GitHub.

Props to Ingvald, Larry, and the Jasmine, Rhino and Envjs teams. You guys rock.

DevOps New Zealand