DEV Community

Heiker
Heiker

Posted on • Updated on

Setting up tape testing framework for basic frontend development

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
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

And finally we require it with -r flag in our tape command.

tape -r './test/setup' './test/**/*.test.js'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And use it with tape.

tape -r 'esm' './test/**/*.test.js'
Enter fullscreen mode Exit fullscreen mode

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
  })
}
Enter fullscreen mode Exit fullscreen mode

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(); 
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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']);

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
  }
}
Enter fullscreen mode Exit fullscreen mode

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 with fs: '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 reading. If you find this article useful and want to support my efforts, buy me a coffee ☕.

buy me a coffee

Top comments (0)