Generally it would be nice to only use one testing framework rather than using
separate front end and back end testing frameworks.
It means you don't have to flit between different syntaxes for tests,
and that you don't have to bloat your devDependencies
.
We're going to learn how to use JSDOM alongside tape to test our front end code.
We're going to have a small example, with some basic DOM manipulation, and we're going to write some tests for it!
If you haven't already, look through the
README.md
of this repo to get introduced to tape.
The example we are following is a basic counter
which increments, decrements and resets.
The complete code is in
front-end-testing/lib
,
open the index.html
in your web browser,
Have a look at it, read through the code in
script.js
and make sure you understand it.
The first thing to note from reading
script.js
is the if
statement at the bottom:
/*istanbul ignore next */
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
increment: increment,
decrement: decrement,
resetFunc: resetFunc,
updateDom: updateDom,
currentCount: currentCount,
};
}
This just stops the browser from trying to process module.exports
so that we don't get any console errors
when the code is run in a browser
(if module
is undefined
then trying to access properties from
it will result in an error).
We need to use module.exports
still
so that we can require it into our test file.
We use /*istanbul ignore next*/
as we do
not want this if statement to effect our coverage.
If you've not used istanbul before check out our
learn-istanbul
repo.
With that if
block of code covered, lets get started on testing!
Inside of the
front-end-testing
directory, make a new directory called test
and inside of the new front-end-testing/test
directory you just created
create a file called test.spec.js
.
Open it in your favourite text editor and lets get started!
First we'll require in tape, JSDOM and fs.
const test = require('tape');
// jsdom is a way to create a document object on the backend, we only need the
// JSDOM constructor from it so we get that out straight away
const JSDOM = require('jsdom').JSDOM;
const fs = require('fs');
We then need to get the contents of our html file so that JSDOM can use it to create its own DOM.
const html = fs.readFileSync(__dirname + '/../lib/index.html', 'utf8');
If you've not used the core node packages before then checkout learnyounode to get some understanding of the basics of node.
We're now going to get the DOM we'll be using set up.
JSDOM is a constructor which takes the argument of a HTML file as a string, it will then create a DOM (document object model) in the same way a browser would, and we assign it to the variable 'DOM'
const DOM = new JSDOM(html);
Next we declare a global variable.
Node is a little different to the browser:
if we want something to be in the global scope (as in, available in other files
whilst they are being processed by 'require') then we need to specifically
declare that. We do this with the 'document' from the DOM we just created
so that it can be used by our JS file.
global.document = DOM.window.document;
⬆️ This takes the document
from the DOM object
and makes it globally available
in the current node environment.
This means that when we require in our script.js
file it won't error due to document
being undefined,
because it is defined
with the DOM we just made!
So now we can require our script file in.
const frontEndCode = require('../lib/script.js');
Now frontEndCode will be an object
which is a copy of what we exported using
module.exports
in script.js
.
So, now let's write some tests!
Our increment and decrement functions both take in a number (or a number as a string), and return that number increased/decreased by one, respectively. If it is passed something which is not a number, or a string which can be coerced to a number, then it should update the DOM to add an error message.
So first off lets write a test for increment for when it's passed the expected arguments.
test('test increment function', function(t) {
const actual = frontEndCode.increment(1);
const expected = 2;
t.equal(actual, expected, 'should add one to a number');
t.end();
});
This test is just like the tests from the README.md
of this repo as we're returning a basic value.
But, as said, if we call it with invalid input it updates the DOM with an error message.
So we can update the test function so it looks like this:
test('test increment function', function(t) {
let actual = frontEndCode.increment(1);
let expected = 2;
t.equal(actual, expected, 'should add one to a number');
frontEndCode.increment('not a number');
// JSDOM does not support the use of 'node.innerText' so we have to use 'node.textContent'
// I can access things in the 'document' just like in the browser
actual = document.querySelector('.error').textContent;
expected = 'Error: Argument passed to increment was not a number';
t.equal(actual, expected, 'should update error node when a string passed in');
t.end();
});
Let's break down what happens here: We call our increment function with invalid input
frontEndCode.increment('not a number');
We now hope that, as the code in our script.js
suggests, the div with the
class error
should now have a text node inside which says "Error: Argument
passed to increment was not a number".
So we can go get the textContent
of that div and assign it to a variable,
in exactly the same way we would in front end code.
actual = document.querySelector('.error').textContent;
(since let actual
has already been declared above
in this function's scope,
we don't have to declare it,
we just have to reassign it.)
Then assign what we expect to a variable:
expected = 'Error: Argument passed to increment was not a number';
And then we compare them like any other test:
t.equal(actual, expected, 'should update error node when a string passed in');
Now, save your file, go to your terminal and run:
npm run front-end-test
You should see something which looks like this:
Now run:
npm run front-end-coverage
You should see something like this:
Our aim (as it always is) is to bring this up to 100% coverage!
Now that we've walked through how to write a test for the increment function,
you should now be able to have a go at writing tests
for the decrement
, resetFunc
, currentCount
, and updateDom
functions!
If you get stuck you can take a look in the
front-end-testing/test-complete
to see how we've written our tests.
Run npm run front-end-test
whenever you write a test to make sure it's passing
(don't forget to use t.end()
).
When you've written tests for all of those functions,
run npm run front-end-coverage
and you should see something like this:
If your coverage is looking lower on any of the options then look at your tests, are you testing all of the functions? Are you testing all possible outcomes of each function?
The last bits to test are our event listeners, since inside of the functions passed as the arguments to the event listeners we compose our functions, we need to make sure this composition is behaving as we expect it to.
We know each function on its own is behaving as expected, as we just wrote passing unit tests for them, but now we want to test how they are put together.
We can do this by simulating a click
on the button!
So, let's start with increment again, set up a new test just as we have been:
test('increment is called properly when the inc button is clicked', function(t) {
});
Now we want to make sure that count
is at zero,
just in case the previous code has effected it,
so we pull in the count node and update it:
let count = document.querySelector('.count');
frontEndCode.updateDom(frontEndCode.resetFunc(), count);
Next we want to simulate a click on our "+" button,
just like in a browser we can call .click()
on any DOM node
to simulate a click on it!
So we can add:
document.querySelector('.inc').click();
And under that we can set up an expected, actual and a test by using our JSDOM.
let actual = count.textContent;
let expected = '1';
t.equal(
actual,
expected,
"clicking the '+' button in the DOM should increment the count by 1"
);
t.end();
You should now have a test that looks like this:
test('increment is called properly when the inc button is clicked', function(t) {
let count = document.querySelector('.count');
frontEndCode.updateDom(frontEndCode.resetFunc(), count);
document.querySelector('.inc').click();
let actual = count.textContent;
let expected = '1';
t.equal(
actual,
expected,
"clicking the '+' button in the DOM should increment the count by 1"
);
t.end();
});
run npm run front-end-test
and watch the test pass. 🎉
Now, write a test for the event listener for the "-" and "reset" buttons, in a really similar way to what we've just done.
Once you've done that run npm run front-end-coverage
again and bask in the
glory of your 100% coverage! Just think of the badge on your repo when you get
this on your own projects!
Now you can use tape for your front end and back end code, and get 100% test coverage everywhere!
If you found this taster to front-end testing with Tape and JSDOM helpful, consider reading our more comprehensive example: github.com/dwyl/todo-list-javascript-tutorial
- JSDOM documentation
- Testing online/offline functionality using JDOM stack overflow answer