Eric Radman : a Journal

Testing JavaScript with HTML Fixtures

Refresh-Button Testing

To meaningfully test JavaScript that is part of a web application one needs to interact with the browser's DOM. Most large and small JS test frameworks approach this problem by creating and document and then adding the tests to the document:

<!doctype html>
<html>
  <head><meta charset="utf-8"></head>
  <body>
  <!-- insert HTML and SVG here -->
  </body>
  <script src="testrunner.js"></script>
  <script src="mycode.js"></script>
</html>
</code>

The obvious problem is that this cannot be easily included in automated tests since a manual browser load and refresh is required.

The second difficulty is that each individual tests require careful setup and teardown if they modify the document they're embedded in. If you are testing client code for a web application it almost certainly does modify the DOM state, which results in interacting unit tests.

Fresh Fixtures in Node.js

For the command line, node is an obvious place to start because it has good exception handling (if you can put up with the long backtraces) and a rich library. Now instead of writing a document with embedded tests we can start writing tests that interact with data (the document):

var assert = require('assert');

The downside to starting with Node is that there is no HTML, CSS or SVG. No DOM. To solve this problem we can use jsdom, a library with nearly 90 dependencies. Once installed we can construct a document:

var jsdom = require("jsdom").jsdom;

Immedately we are faced with the sad fact that JavaScript does no have the concept of a `heredoc`. This can be emulated in heroic ways by calling the `.toString()` method on a function containing a large comment.

function hereDoc(f) {
  return f.toString().
      replace(/^[^\/]+\/\*!?/, '').
      replace(/\*\/[^\/]+$/, '');
}
var html = hereDoc(function() {/*!
<!doctype html>
<html>
...
</html>
*/});

Alternatively we could load the string from a file. Now we'll include the library we want to test:

var app = require('../public/ui.js');

We still have to choose a test runner. The following example uses Mocha. The important part is that we get a new jsdom object instantiated before each test, so careful setup or teardown is not required:

describe("app", function() {
    var window = null;

    beforeEach(function(){
        window = jsdom(html).defaultView;
    });

    describe("test 1", function() {
        it("contains an embeded product ID", function () {
            content_div = window.document.getElementById('content')[0];
            assert.equal(chart_div.innerHTML, "...");
        });
    });
});

Native Browser Tests with phantom-assert

At the time of this writing jsdom no longer supports node.js, and requires io.js instead. If this is or a lengthy chain of NPM modules is not an option in your environment then it is possible to write automated tests using a test runner for phantomjs. The project I crated to prove this concept is phantom-assert, and works by injecting each test function into the page and resetting the page content inbetween each test. A simple example looks like this:

var html = "fixture.html";
var tests = [];

tests.push(function count_chart_elements () {
    assert(document.getElementsByClassName('chart').length, 1);
})

Using this model is impossible to create interacting tests because the page content is refreshed after each test.%em phantom-assert is not a micro-framework, it is a polished example who's source code can be read in less than 5 minutes. This level of minimalism eliminates the need for configuration because the framework itself can be easily modified and embedded directly into your project.

Other Test Frameworks

If you are interested in a more complete test harness, klud.js is a micro-framework that may fit your needs.

For functional testing or any testing on PhantomJS, CasperJS is full-featured and well supported.

Last updated on November 26, 2016