Tom MacWright

tom@macwright.com

TAP & Tape, the awesome way to test JavaScript

Update: This post was published in 2014. As of 2017,
  • tape is still great and recommended for many projects.
  • tap is more actively maintained and has better support for code coverage and built-in pretty reporters. It includes those things at the cost of not supporting browsers, and being a bigger dependency.
  • I also use Jest often because it has good support for JSDom "browser-like" testing and integration with Babel for testing ES2017.

node-tap & tape are simple, awesome testing tools for JavaScript. The JavaScript community has grown up with testing culture, and the vast majority of projects use larger tools like Mocha and Jasmine. Recently I’ve been switching lots of projects over to tap & tape, and want to share why.

substack, the creator of tape, has already written a bit about his process and thinking - here’s just a little more, written from the perspective of a former mocha user.

What it sorts down into is roughly three parts: the protocol, browserify, and magic.

The Test Anything Protocol

TAP, or the Test Anything Protocol is the definition of ‘tried & true’: it’s been around since 1987 and has been implemented in a ton of languages. It’s just a dead-simple way to format test results, like

TAP version 13
# equivalence
ok 1 these two numbers are equal

1..1
# tests 1
# pass  1

# ok

Having this format be super simple means that you can combine tools that have common expectations. For instance, faucet is a node module that gives pretty, summarized results for your tests, but it doesn’t directly plug into tape or node-tap - all it cares about is the TAP protocol, so you can pipe results into it. You could even pipe a TAP producer in a different language into it, and it’ll work just the same.

Browserify

browserify is another awesome tool written by substack. With it, you can use node-style require() calls in code you’re going to run in browsers, and then use the browserify command to stitch them all together and make something you can throw in a script tag.

Browserify has been huge for writing cross-platform libraries, and it’s been huge for building things. Mapbox.com and Mapbox.js are both constructed this way - individual libraries on npm, package.json just like nodejs code, and then browserify to bring it to the web.

Long story short, browserify ‘just works’ with tape. While mocha has something of an unusual browser story - it’s hard to get at the files you’d want to run tests in a browser, and then when you do, if you want to run tests in both browsers and node, it’s not straightforward to conditionally require things sometimes.

So, to run browserify tests in a browser, you can just run

$ npm install -g browserify testling
$ browserify test/test.js | testling

Simple as that. Since tape is just a node module and browserify turns the whole thing into just JavaScript, it’s easy to run it anywhere you want - embedded in a webpage, in a browser, or wherever. tape uses console.log to write its results, which is super easy to pull out of a headless browser.

Concepts

A quick review: TAP is a standard for test output. node-tap and tape are two node modules that let you write tests that output results in the TAP protocol.

So, we’ve discussed TAP a little bit, and you might notice that mocha supports TAP too. So why not just use mocha to write tests? Let’s talk about that.

Magic

Mocha does a little magic. With only few exceptions, nodejs has the assumption that any variables on a page will come from obvious places:

// this comes from a module
var module = require('module');

In the interest of simplicity, mocha doesn’t follow this rule. Your mocha test files have assumptions:

var assert = require("assert")
describe('truth', function(){
  it('should find the truth', function(){
    assert.equal(1, 1);
  })
})

Keen eyes will notice that assert entered the stage by a require() call, but describe and it didn’t - they appear magically.

On the other hand, a basic tape test:

var test = require('tape').test;
test('equivalence', function(t) {
    t.equal(1, 1, 'these two numbers are equal');
    t.end();
});

test comes from require(), and t comes from the closure. Simple enough. This lack of abstraction has two awesome advantages:

  1. It’s easier to form a mental model of what’s going on, so it’s easier to hack with it and know what’ll happen. What if I call a function with t and then call functions off of t? It’ll work.
  2. Tests are code. So, you can run tests as modules with node, just like you’d run other code: node test/test.js, and it works. As opposed to needing a ‘test runner’ binary that contains some of the code the test really needs.

Testling & Travis-CI

Continuous Integration, where every commit to GitHub is automatically tested, has become a necessity. Setting the green ‘this works’ badge on projects means something, and we’ve found that running tests on remote hosts can give a better sanity check than just running them locally - the environment is constructed from scratch, there aren’t any stray files that make things work.

tape works with testling just like mocha - the same minimal .travis.yml file ‘just works’ as long as npm install and npm test do their things. But on top of that, you can use Testling-CI and testling, and test commits in browsers. Testling-CI works the same as Travis: set a webhook, tweak a few package.json properties, and you’ll get a page of your own with results. testling, on the other hand, runs headless tests locally, with your real browsers, not with phantomjs or another custom abstraction. In combination, this means that you can easily cross-browser test code. And if the abstractions break, it’s easy to just browserify the code and run the tests manually.