Today we are going to learn how we can use tape to test code that is meant to run in a browser.
You can checkout the source code examples on github
What is tape?
Tape is a javascript testing framework that provides only essential feature set so you can make assertions about your code.
Why use tape?
This is the part where I try to sell you tape, but I wont do that.
If you navigate in the interwebs in search for more information about it, you'll probably find someone that tells you that the simplicity of this framework will magically make your test (and your whole codebase) more maintainable. Please don't fall for that.
If you find yourself needing to mock ajax calls, or websocket connections, or need to monkey patch your module requires, then I suggest you start to look for a more "feature complete" testing framework like jest. Or checkout cypress.
Use tape if you see that the limited features it provides fit your needs.
Lets use the stuff
Begin with installing tape.
npm install -D tape@5.2.2
Now for a test drive we will create a simple.test.js
file inside of a folder named test
. Next, we create a test.
// ./test/simple.test.js
var test = require('tape');
test('1 + 1 equals 2', function(t) {
var sumResult = 1 + 1;
t.equals(sumResult, 2);
t.end();
});
So what's happening in here?
On the first line we require tape
, like we would any other module within our "regular" codebase. Then we store the only function that it exposes in a variable. We are using require
and not import
for now, but we'll fix that later.
Then we call test
. The first parameter is a title, a string that should describe what we are testing. The second parameter is the actual test, which we pass as a callback.
You'll notice that we get an object in our callback. This object is our assertion utility. It has a set of methods that display useful messages when the assertions fail. In here I'm calling it t
because that's how you find it in the documentation.
Finally we explicitly tell tape that the test needs to end using t.end()
.
What's interesting about tape is the fact that is not some super complex testing environment. You can execute this test like any other script using node. So you could simply write node ./test/simple.test.js
on the terminal and get the output report.
$ node ./test/simple.test.js
TAP version 13
# 1 + 1 equals 2
ok 1 should be equal
1..1
# tests 1
# pass 1
# ok
If you want to execute more than one test file you can use the binary that tape provides. This will give you access to a command named tape
and pass a glob pattern. For example, to execute every test file that match anything that ends with .test.js
inside a folder named test
, we could write an npm script with this:
tape './test/**/*.test.js'
Using ES6 modules
There is a couple of ways we can achieve this.
With babel-register
Warning: This wont work with node versions that have native support for ES modules. I think this includes Node 12.17 and beyond.
If you have babel already installed and configured with your favorite presets and plugins, you can use @babel/register
to compile your testing files with the same babel config that you use for your source code.
npm install -D @babel/register@7.0.0
And then you can use the tape
command with the -r
flag to require @babel/register
. Like this:
tape -r '@babel/register' './test/**/*.test.js'
With require hooks
Warning: This wont work with babel 7.
Another approach to solve this is by using require-extension-hooks in a setup script.
npm install -D require-extension-hooks@0.3.3 require-extension-hooks-babel@0.1.1
Now we create a setup.js
with the following content.
// ./test/setup.js
const hooks = require('require-extension-hooks');
// Setup js files to be processed by `require-extension-hooks-babel`
hooks(['js']).plugin('babel').push();
And finally we require it with -r
flag in our tape
command.
tape -r './test/setup' './test/**/*.test.js'
With esm
We could still use import statements even if we don't transpile our code. With the esm package we can use ES6 modules in a node environment.
npm install -D esm@3.2.25
And use it with tape.
tape -r 'esm' './test/**/*.test.js'
For more information about
esm
see this article
Testing the DOM
Imagine that we have this code right here:
// ./src/index.js
// this example was taken from this repository:
// https://github.com/kentcdodds/dom-testing-library-with-anything
export function countify(el) {
el.innerHTML = `
<div>
<button>0</button>
</div>
`
const button = el.querySelector('button')
button._count = 0
button.addEventListener('click', () => {
button._count++
button.textContent = button._count
})
}
What we got here (besides a disturbing lack of semicolons) is an improvised "component" that has a button that counts the number of times it has been clicked.
And now we will like test this by triggering a click event in this button and checking if the DOM actually updated. This is how I would like to test this code:
import test from 'tape';
import { countify } from '../src/index';
test('counter increments', t => {
// "component" setup
var div = document.createElement('div');
countify(div);
// search for the button with the good old DOM API
var button = div.getElementsByTagName('button')[0];
// trigger the click event
button.dispatchEvent(new MouseEvent('click'));
// make the assertion
t.equals(button.textContent, '1');
// end the test
t.end();
});
Sadly if we try to run this test it would fail for a number of reasons, number one being that document
doesn't exists in node. But we'll see how we can overcome that.
The fake DOM way
If you would like to keep executing your test in the command line you could use JSDOM in order to use a DOM implementation that works in node. Because I'm lazy I'll be using a wrapper around JSDOM called browser-env to setup this fake environment.
npm install -D browser-env@3.3.0
And now we create a setup script.
// ./test/setup.js
import browserEnv from 'browser-env';
// calling it this way it injects all the global variables
// that you would find in a browser into the global object of node
browserEnv();
// Alternatively we could also pass an array of variable names
// to specify which ones we want.
// browserEnv(['document', 'MouseEvent']);
With this in place we are ready to run the test and watch the results.
$ tape -r 'esm' -r './test/setup' './test/**/*.test.js'
TAP version 13
# counter increments
ok 1 should be equal
1..1
# tests 1
# pass 1
# ok
But what if you don't trust in JSDOM or simply think is a bad idea to inject global variables in the node process that runs your test, you can try this in different way.
Using the real deal
Because tape is a simple framework it is posible to run the test in a real browser. You may already be using a bundler to compile your code, we can use that to compile our test and run them in the browser.
For this particular example I will show the minimum viable webpack config to get this working. So lets start.
npm install -D webpack@4.46.0 webpack-cli@4.6.0 webpack-dev-server@3.11.2 html-webpack-plugin@4.5.2
Let the config begins...
// ./webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { join } = require('path');
module.exports = {
entry: join(__dirname, 'test', 'simple.test.js'),
mode: 'development',
devtool: 'inline-source-map',
plugins: [
new HtmlWebpackPlugin()
],
node: {
fs: 'empty'
}
}
Let me walk you through it.
-
entry
is the test file that we want to compile. Right now this entry point is a test file, but you can leverage webpack features to bundle multiple test files. -
mode
is set in development so webpack can do its magic and make fast incremental builds. -
devtool
is set to inline-source-map so we can debug the code in the browser. -
plugins
we only have one, the html plugin creates an index.html file that is used by the development server. -
node
is set withfs: 'empty'
because tape uses this module in their source, but since it doesn't exists in the browser we tell webpack to set it as an empty object.
Now if you use the webpack-dev-server
command, and open a browser on localhost:8080
you'll see nothing but if you open the browser console you'll see tape's test output.
Other sources
Thank you for your time. If you find this article useful and want to support my efforts, consider leaving a tip in ko-fi.com/vonheikemen.
Top comments (0)