DOM TDD with JSDOM¶
We earlier saw, in TDD with Mocha, how we can start on JavaScript TDD using Mocha. We used a very simple application and test.
In this section we show how frontend tooling can combine to provide a TDD workflow targeted at browsers, using a fake DOM from the jsdom project.
Overview¶
- Explain the need for a DOM in frontend TDD
- Install and setup
jsdom
- Use Mocha setup/teardown hooks
- Make a “helpers” file to re-use common setup
Fake DOM¶
We have been doing JavaScript TDD in Node, a headless environment. But our goal is to write frontend application that run in the browser. That means a DOM, plus some other non-Node machiner such as XHR.
For example, many frameworks, such as jQuery, expect there to be a DOM.
Your code won’t even import without some globals for a window
and
a document
.
Fortunately there are solutions, such as
jsdom which simulate what you need.
With jsdom
, we can resume our Pythonic workflow: sit in PyCharm,
writing tests which import code and make assertions.
Getting a DOM¶
Let’s mix jQuery into the our incrementor
from the Mocha Intro article
and see what happens. First we install it from npm and save it as a dependency
in our package.json
:
$ npm install --save jquery
We can now change our application code: instead of a function that returns an
incremented value, we increment the text node value of a <div>
:
var $ = require('jquery');
function incrementer (i) {
$('div').text(i+1);
}
module.exports = incrementer;
Our application code imports jquery
using NodesJS/CommonJS
module imports, then changes the <div>
content to equal
the incremented value.
We re-use the previous section’s test4.js
as test1.js
in this
article:
var describe = require('mocha').describe,
it = require('mocha').it,
expect = require('chai').expect,
incrementer = require('./app');
describe('Hello World', function () {
it('should increment a value', function () {
var result = incrementer(8);
expect(result).eql(9);
});
});
When we run the test now, though, armageddon ensues:
Error: jQuery requires a window with a document
at module.exports (jsdom/node_modules/jquery/dist/jquery.js:29:12)
at incrementer (jsdom/app.js:4:5)
at Context.<anonymous> (jsdom/test1.js:6:22)
Our first thought is: go get a browser. We could use PhantomJS which has good package for Mocha support. We could start over with the Karma test runner. But these are big solutions. Slow, with lots of fiddling necessary, and not all headless.
Enter jsdom. This package simulates a DOM, in your browser. While jsdom isn’t perfect in simulating a browser, it is fast and, relatively speaking, lightweight.
Let’s install it as a development dependency:
$ npm install jsdom --save-dev
We now can write a test2.js
which imports jsdom
and sets some
global variables that jQuery
expects. With that in place, we can import
jQuery
:
var describe = require('mocha').describe,
it = require('mocha').it,
expect = require('chai').expect,
jsdom = require('jsdom');
global.document = jsdom.jsdom('<body><div>1</div></body>');
global.window = document.defaultView;
var $ = require('jquery');
var incrementer = require('./app');
describe('Hello World', function () {
it('should start with 1', function () {
expect($('div').text()).equal('1');
});
it('should increment to 6', function () {
incrementer(5);
expect($('div').text()).equal('6');
});
});
This test suite has a test that ensures we are setup correctly by
reading the initial text value of the <div>
. The second test
executes our function and checks the updated value of the <div>
.
And our tests pass! All is good...except, it isn’t.
Mocha Setup and Teardown¶
Python unit testers will spot the problem quickly: we aren’t testing
in isolation! The second test modifies the <div>
. Any subsequent
tests wouldn’t be against a fresh <div>
. If we added a third
test as a copy of the first, we’d see that:
var describe = require('mocha').describe,
it = require('mocha').it,
expect = require('chai').expect,
jsdom = require('jsdom');
global.document = jsdom.jsdom('<body><div>1</div></body>');
global.window = document.defaultView;
var $ = require('jquery');
var incrementer = require('./app');
describe('Hello World', function () {
it('should start with 1', function () {
expect($('div').text()).equal('1');
});
it('should increment to 6', function () {
incrementer(5);
expect($('div').text()).equal('6');
});
it('should start with 1', function () {
expect($('div').text()).equal('1');
});
});
This third test fails, as the <div>
has the value from the second
test, not the initial value.
Like Python’s unittest
, Mocha has concepts of before
,
beforeEach
, and afterEach
. Let’s say we want to balance speed
and isolation. We’d like to make a DOM once for all tests, but clean
up the <body>
before each test. test4.js
shows this:
var describe = require('mocha').describe,
it = require('mocha').it,
expect = require('chai').expect,
before = require('mocha').before,
beforeEach = require('mocha').beforeEach,
jsdom = require('jsdom');
describe('Hello World', function () {
var $, incrementer;
before(function () {
global.document = jsdom.jsdom('<body></body>');
global.window = document.defaultView;
$ = require('jquery');
incrementer = require('./app');
});
beforeEach(function () {
$('body').html('<div>1</div>');
});
it('should start with 1', function () {
expect($('div').text()).equal('1');
});
it('should increment to 6', function () {
incrementer(5);
expect($('div').text()).equal('6');
});
it('should start with 1', function () {
expect($('div').text()).equal('1');
});
});
Our Hello World
test suite initializes $
and incrementer
at the test-suite scope. The before
function runs once,
loading our application code once a DOM is setup and initialized. Then,
before each test, the <body>
is reset to <div>1</div>
.
Does this look like boilerplate that you’ll repeat in each test? Let’s
make a helper.js
module that we can import at the top of all of
our tests, to provide such initialization:
var jsdom = require('jsdom');
global.document = jsdom.jsdom('<body></body>');
global.window = document.defaultView;
Our tests, as shown in test5.js
, now look a lot nicer by importing
helper.js
at the top:
require('./helper');
var describe = require('mocha').describe,
it = require('mocha').it,
expect = require('chai').expect,
beforeEach = require('mocha').beforeEach,
$ = require('jquery'),
incrementer = require('./app');
describe('Hello World', function () {
beforeEach(function () {
$('body').html('<div>1</div>');
});
it('should start with 1', function () {
expect($('div').text()).equal('1');
});
it('should increment to 6', function () {
incrementer(5);
expect($('div').text()).equal('6');
});
it('should start with 1', function () {
expect($('div').text()).equal('1');
});
});
Wrapup¶
This turned out to be pretty simple. Suspiciously simple, in fact. As it
turns out, this is one of those areas where frontend development is in
constant churn. jsdom
won’t always work, and other approaches will
have features that you can’t live without.
Still, the basics of this article apply: you can do JavaScript TDD, not just for server-side JavaScript in Node, but also frontend JavaScript in browsers. It takes some patience to get it setup, but it sure beats the normal browser development cycle of:
- Change code
- Switch to browser
- Reload
- Sprinkle
console.log
statements - Sacrifice goats that nothing else broke
- Previous topic: Pythonic JavaScript with ES2015
- Next topic: Initial ToDoMVC