Fully Headless JSSpec

Update 2009-05-12 - With the release of Blue Ridge 1.0, fully headless JavaScript testing just got a whole lot easier! Check out the plugin for the current state-of-the-art approach to unit testing JavaScript in your Rails projects.

I have been frustrated at the state of testing for JavaScript. There are a bevy of great (and not-so-great) choices for testing your JavaScript, but nothing that is current, effective, and automated. We used Crosscheck for a while, but the project went stagnant and was awfully heavy-weight besides. On top of which, it didn't offer a BDD-flavored spec syntax.

I've been a fan of JSSpec for a while, but its "runs tests only in the browser" style means that automated server-side, continuous testing was nigh upon impossible.

Then, along came John Resig and his marathon weekend of hacking to mock out the DOM API in a relatively simple JavaScript file and voila! Headless JSSpec testing for Rails apps could become a reality.

If you are interested in trying it out, first download and install Rhino 1.7R1. Make sure you put the js.jar file on a globally accessible path somewhere. Then, get a copy of my javascript_testing repo at Github (http://github.com/relevance/javascript_testing/tree/master). It is a minimal repo, with just enough to get you started. The files in the repo are:

  • env.js -- a clone of John Resig's browser environment file, with a couple of minor tweaks for Script.aculo.us support
  • test_jsspec.rake -- a couple of rake tasks for running the tests, and for integrating them with CruiseControl (will add support for other CI solutions as I can)
  • jsspec/jsspec.js -- an unmodified copy of the jsspec trunk, here for your convenience
  • jsspec/config.js -- a series of, essentially, global includes for your specs. Edit to suit your own environment
  • jsspec/consoleReportForRake.js -- an extension to JSSpec that adds an afterAll hook for reporting errors that blow up your Rake task appropriately (for continuous integration purposes)
  • jsspec/prototypeForJsspec.js -- a minor tweak to Prototype to get it working with the env.js file

To get rolling, copy everything but the rake task into RAILS_ROOT/test/javascripts. Put the rake task in RAILS_ROOT/lib. Edit jsspec/config.js to load any standard JavaScript files your project uses. Then, start writing specs. Here's an example, RAILS_ROOT/test/javascripts/spec_string.js:

load("jsspec/config.js"); 
with(Spec) {  
   describe("Prototype's String.stripTags", function() { with(this) {  
   
     it("should remove open and close tags", function() {  
        ("<h1>text</h1>".stripTags()).should(equal("text"));
     });
   
     it("should remove self-closing tags", function() {
        ("text<br/>".stripTags()).should(equal("text"));
      
     });
   }});
};

Specs.report = "ConsoleReport";  
Specs.run();

To run this, use the targeted version of the rake task:

:trunk:-> rake test:jsspec TEST=spec_string.js
(in /Users/jgehtland/RELEVANCE/project/trunk)
.
.

2 passed. 

Run 2 examples in 0.001seconds.

:trunk:-> 

(Simply running rake test:jsspec will run any file in your test/javascripts folder whose name begins with "spec" and report the results.)

We don't actually encourage you to write specs and tests for standard libraries like Prototype, JQuery, etc. It just makes for an easy demo.

If you want to test something that requires some HTML to bind against, I generally create a folder called test/javascripts/fixtures, and load HTML exemplars in there. For example, to test Prototype's $$ selector, you might create a fixture called fixtures/selector.html that contains the following:

<html>
<body>
  <div class="select_me"/>
  <span class="select_me"/>
  <div class="dont_select_me"/>
</body>
</html>

Your test, spec_selector.js, would now contain:

load("jsspec/config.js"); 
window.location = "fixtures/selector.html";
load("jsspec/jsspec.js");

with(Spec) {  
   describe("Prototype's $$ selector", function() { with(this) {  
   
     it("should find only elements with the provided class selector", function() {  
        ($$('.select_me').length).should(equal(2));
     });
   
     it("should find only elements with the provided tag", function() {
       ($$('div').length).should(equal(2));
     });
   
   }});
};

Specs.report = "ConsoleReport";  
Specs.run();

If you want these tests to be part of your regular CI process, just uncomment the second task in test_jsspec.rake and you can now have CI for your JS, ASAP.

Get In Touch