There are many ways to test code producing code coverage stats. But mostly based on fake DOM environments or, in other words, without a browser.

The DOM Code Coverage Essentials

To properly test a page, a component, or the whole App, we need a web server, a tool able to understand JS and instrument it in a way that running it would produce coverage information, and finally a browser.

My choice for each of these essentials tool is the following one:

The example folder

For demo purposes, I have created a very simple structure.

The library is inside lib, and everything else is inside the test one.

The extra test/lib is needed as target for instrumented code.

You can play around with the structure that better suite you.

Code Covering a DOM Utility

Instead of talking about code coverage on DOM, we are going to write a utility and cover it as we go.

The utility is a classic document.querySelector/All helper, described as such:

const $ = (css, p) => (p || document).querySelector(css);
const $$ = (css, p) => (p || document).querySelectorAll(css);

It does something very simple: it returns either a single node, or a list of nodes. I have put the content of this file inlib/index.js .

Test Basics

To keep the project clean and well organized, I have created a folder test that will contain all the test related things, including test/index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="lib/index.js"></script>
</head>
<body>
<p>first</p>
<p>second</p>
</body>
</html>

This page is the basic structure we’ll use to test the two utilities.

Since the lib folder does not exist inside the test one, we need to create it, and this is also the time we provide an instrumented version of the library.

Istanbul Basics

Istanbul is a very simple to use code-coverage tool able to do everything at once, providing also the ability to perform subtasks, like instrumenting.

After a basic npm init -y in the root folder, and after running the usual command npm install --save-dev istanbul to bring in the too, we need to add a script entry dedicated to instrument any file of our lib folder into test/lib

"scripts": {
"instrument": "istanbul instrument ./lib -o ./test/lib",
"test": "..."
}

We can try to see if everything is fine via npm run instrument , which should create a different test/lib/index.js file for us.

Nightmare Basics

Nightmare is a super simple way to test an instance of Chrome/ium, headless or not, against a generic file or site.

npm install --save-dev nightmare

It takes nothing to create a nightmare instance, and the rest of the code can be saved as test/index.js since it’s our test entry point.

const Nightmare = require('nightmare');
const nightmare = Nightmare({show: true});

While the API exposes a lot of cool functionalities, there’s apparently no way to intercept console.assert calls, so we need to create a basic assert utility able to log on the console, the node one, as opposite of the browser one, everything we’d like to assert.

nightmare
.goto(`http://localhost:8080/`)
// basic assert utility (console.assert is not intercepted)
.evaluate(() => {
window.assert = (ok, message) =>
console.warn('nightmare', !!ok, message || 'unknown');
})
// provide a mechanism to intercept asserts
.on('console', (type, ...args) => {
if (type === 'warn' && args[0] === 'nightmare') {
type = 'assert';
args.shift();
}
switch (type) {
case 'assert':
const [ok, message] = args;
if (!ok) exit(new Error(message));
else console.log(` \x1B[0;32m✔\x1B[0;0m ${message}`);
break;
case 'error':
exit(new Error(args[0]));
default:
console[type](...args);
}
})

At this point, all we need to remember is to return window__coverage__ at the end of our test and write it on a file.

// ... previous code ... 
// return at the end the coverage result
.evaluate(() => window.__coverage__)
.end()
.then(coverage => {
require('fs').writeFile(
require('path').join(__dirname, 'coverage.json'),
JSON.stringify(coverage),
(err) => {
if (err) exit(err);
}
);
})
.catch(exit)

In case you are wondering what is the exit function, here it comes:

function exit(error) {
console.error(error);
process.exit(1);
}

The http-server

The last piece of the puzzle we need, is npm install --save-dev http-server.

Once we have that too, we can replace the default package.json test entry with the following one (in one line only, of course):

"test": "http-server -s test & node test && istanbul report --include=test/coverage.json text-summary"

Above command will launch http-server in a silent mode (that doesn’t work here, btw) then via & that avoids waiting for it to close, will execute the test folder via node, and show the resulting report as text-summary.

Please note when you use & instead of && the command gets detached so if you need to kill previous http-server run remember to killall node once you’ve done testing. Worth mentioning there are tools to execute in parallel.

Anyway, without any test in place we should see something like the following.

console output until now

Start Covering With Tests

I’ve already explained how to test code via vanilla JS, and here I’d like to preserve the simplicity of console.assert , which is unfortunately ignored by nightmare, but it’s provided as global assert one, so let’s start test-covering.

.on(...)
// execute tests
.evaluate(() => {
assert(
$('p').textContent === 'first',
'$ returns the first node'
);
})
.evaluate(...)

Putting an extra code-in-browser evaluation, and checking that $('p') returns the expected node with its expected content, would already rise the coverage to 50%.

====== Coverage summary ======
Statements : 75% ( 3/4 )
Branches : 50% ( 2/4 )
Functions : 100% ( 0/0 )
Lines : 100% ( 2/2 )
==============================

The reason is simple: we’ve tested only one of the utility so that adding the following test would already score 100%.

assert($$('p').length === 2, '$$ returns a list of nodes');

Running npm test again should result into 100% code coverage 🎉

One more thing …

Tools are cool and everything, but tools are also dumb.

For code coverage purpose, we surely reached all parts of the utility, but we haven’t actually covered all possible cases.

The (p || document) part automatically covers the function parameters where (css, p) that last p will always be undefined.

That means we use the p , and we fallback through the || , but from usage perspective, we’re still not sure if passing a second argument would actually produce the expected result.

This might look like excessive or probably not needed, since the coverage grants that if we pass a second argument, the || won’t be reached so all is good, but from a dev perspective, it takes nothing to add another test so … why not?

assert(!$('p', document.head), '$(css, parent) query the parent');

That’s it, now we know that everything works as well as every optional argument.

Web, Mobile, IoT, and all JS things since 00's. Formerly JS engineer at @nokia, @facebook, @twitter.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store