Skip to content

Instantly share code, notes, and snippets.

@seanh
Last active June 3, 2022 19:24
Show Gist options
  • Save seanh/19e682776b2716283983abda78e6f8aa to your computer and use it in GitHub Desktop.
Save seanh/19e682776b2716283983abda78e6f8aa to your computer and use it in GitHub Desktop.
My notes on the Mocha JavaScript test framework

Mocha

Installation

To run this gist install git, node and npm and then:

$ git clone https://gist.github.com/19e682776b2716283983abda78e6f8aa.git
$ cd 19e682776b2716283983abda78e6f8aa
$ npm it

To run the browser example:

$ npx http-server -o

Mocha

Mocha is a JavaScript test framework that provides:

  • Functions for registering tests (describe(), context(), it() and specify()) ("describing your tests") and for running setup and teardown before and after tests (before(), after(), beforeEach(), and afterEach())
  • A command line interface for running tests with Node.js
  • A JavaScript interface for running tests in the browser
  • A few other bits and bobs, like writing asynchronous tests with done() or by returning promises or using async and await; running only some tests with only() and skip() (or with command line options)

Similar tools are Jasmine and QUnit.

Mocha does not provide:

  • Functions for doing assertions. By default, when running tests on the command line, it'll use Node.js's built-in assert. Or you can use an assertions library like Chai.
  • Spies, mocks or stubs. Use Sinon.JS.
  • Coverage. Although there's a mocha-html-cov-reporter npm package. Or you can use another coverage tool.

It seems to be possible to use a different test runner, such as Karma, with Mocha instead of using Mocha's own command line interface and browser API. I'm not sure why you would do that.

Use in browser

Mocha has built in support for running your tests in a browser or on the command line. Running in a browser is easy, you just:

  1. Include Mocha's CSS with a <link href="...mocha.css" rel="stylesheet">
  2. Put an empty <div id="mocha"></div> somewhere in your HTML body
  3. Include Mocha's JavaScript with a <script src="...mocha.js"></script>
  4. Include an assertions library to use with Mocha, such as Chai, with a <script src="...chai.js"></script>
  5. Call <script>mocha.setup('bdd')</script>
  6. Register your tests using describe(), it(), chai.assert(), etc
  7. Call mocha.run()

Complete example:

<html>
  <head>
    <meta charset="utf-8">
    <title>Mocha Browser Example</title>
    <link href="https://cdn.rawgit.com/mochajs/mocha/2.2.5/mocha.css" rel="stylesheet" />
  </head>
  <body>
    <div id="mocha"></div>
    <script src="https://cdn.rawgit.com/mochajs/mocha/2.2.5/mocha.js"></script>
    <script src="http://chaijs.com/chai.js"></script>
    <script>
      mocha.setup("bdd");

      describe("my code", function() {
        it("does something", function() {
          chai.assert.isTrue(true);
        });
      });

      mocha.run();
    </script>
  </body>
</html>

You can pass other options to mocha.setup(), see https://mochajs.org/#browser-configuration.

Use on command line

You can run your Mocha tests on the command line with Node.js:

  1. Install Node.js and npm

  2. Have a JavaScript file or files, such as test.js, where you write all your tests using describe(), it(), assert(), etc. You need to require("assert"):

    let assert = require("assert");
    
    describe("my code", function() {
      it("does something", function() {
        assert.equal(true, true);
      });
    });

    When we were running Mocha in the browser we had to include an assertions library such as Chai and use chai.assert. On the command line Mocha will use Node.js's built-in assert by default, so you can just use assert without having to require() any assertions module. You can still require() and use Chai or any other assertions library instead if you want (you'll need to add Chai to package.json as a dev dependency).

  3. Have a package.json file for your package in which you list Mocha as a dev dependency and give mocha as the test command:

    {
      "name": "19e682776b2716283983abda78e6f8aa_mocha",
      "version": "1.0.0",
      "description": "",
      "main": "test.js",
      "scripts": {
        "test": "mocha"
      },
      "repository": {
        "type": "git",
        "url": "git+ssh://[email protected]/19e682776b2716283983abda78e6f8aa.git"
      },
      "author": "Sean Hammond",
      "license": "ISC",
      "bugs": {
        "url": "https://gist.github.com/19e682776b2716283983abda78e6f8aa"
      },
      "homepage": "https://gist.github.com/19e682776b2716283983abda78e6f8aa",
      "devDependencies": {
        "mocha": "^5.0.0"
      }
    }
  4. Install and run:

    $ npm it
    npm notice created a lockfile as package-lock.json. You should commit this file.
    npm WARN [email protected] No description
    
    added 24 packages in 0.767s
    
    > [email protected] test /home/seanh/Gists/19e682776b2716283983abda78e6f8aa_mocha
    > mocha
    
    
    
      my code
        - pending test
        ✓ does something
    
    
      1 passing (3ms)
      1 pending

    It automatically finds your tests by looking for ./test/*.js files and executing them. It also seems to find test.js. And see the --recursive option.

    (If you don't have any other dependencies besides mocha to put in your package.json then you can just skip the package.json and run the tests with npx mocha instead of npm it.)

You can also use Mocha programmatically from your own Node.js script, instead of using the mocha command from a shell.

Command line arguments

https://mochajs.org/#usage

If using npm test insert an additional -- before these to pass the arguments to mocha rather than to npm, example:

$ npm test -- --reporter dot

Some of the more interesting ones:

  • --recursive to look for tests in subdirs as well, probably need this
  • -w, --watch to automatically re-run tests when files change
  • --bail bail after first test failure
  • -d, --debug use node's debugger (doesn't seem to work)
  • --inspect activate devtools in Chrome (?)
  • -g, --grep <pattern> to only run tests matching <pattern> (if running tests in the browser you can use this with a ?grep=<pattern> query string) or -f, --fgrep <string> to only run tests containing <string>, and -i, --invert to invert the --grep and --fgrep matches, instead of using only() and skip() in the code

BDD, TDD, etc

Mocha supports various different interfaces or styles of functions for registering your tests. They're all equivalent, just with different names for the same functions and different ways of calling them.

The default interface is BDD-style functions for registering tests (describe(), context(), it(), specify(), before(), after(), beforeEach(), and afterEach()) (context() is an alias for describe(), and specify() is an alias for it()).

Alternatively you can use TDD-style which is just the same functions but with different names: suite(), test(), suiteSetup(), suiteTeardown(), setup(), and teardown().

Or a QUnit style: suite(), test(), before(), after(), beforeEach(), and afterEach(), and you don't nest the test()s inside the suite()s.o

Or a require style in which you have to require() describe, before, it etc instead of just having them available as globals.

There's also a module.exports style:

module.exports = {
  before: function() {
    // ...
  },
  'Foo': {  // Equivalent to a describe()
    '#bar()': {  // Equivalent to a describe()
      'should return -1 when not present': function() {  // Equivalent to an it()
        [1,2,3].indexOf(4).should.equal(-1);
      }
    }
  }
};

describe(), context(), it() and specify()

describe() should usually be passed the name of a class, function or method, and can be nested. # is used before method names. Examples from the Mocha docs:

describe('Array', function() {
  describe('#indexOf()', function() {
  describe('#concat()', function () {
  describe('#slice()', function () {

describe('User', function() {
  describe('#save()', function() {

describe('#find()', function() {

describe('add()', function() {  // Just a top-level function.

There are a couple of examples in the docs that deviate from this style. For example describing an API:

describe('api', function() {
  describe('GET /api/users', function() {

describe('app', function() {
  describe('GET /users', function() {

And things like:

describe('hooks', function() {
describe('retries', function() {

context() is an alias for describe(), to be used to group certain contexts within a describe(). Examples from docs:

describe('#indexOf()', function() {
  context('when not present', function() {
  context('when present', function() {

Works particularly well with different beforeEachs inside different context()s.

it() is for the test functions themselves. "It should" is by far the most common style in the docs:

it('should not throw an error', function() {
it('should return -1', function() {
it('should return the index where the element first appears in the array', function() {

Or just "it <VERB>":

it('responds with matching records',
it('correctly adds ' + test.args.length + ' args', function() {

specify() is just an alias for it(). No examples are given in the docs, but I think you would use it when it's awkward for the sentence describing your test to begin with "It".

Hook functions: before(), after(), beforeEach() and afterEach()

  • Nesting inside nested describes and contexts

  • A single block can have multiple of each, so you can split them up by what they do.

  • You can pass named functions to aid with code readability and debugging:

    beforeEach(function namedFun() {
      // beforeEach:namedFun
    });

    This makes the code more readable by documenting the intention of the hook function and makes the code easier to debug because if something goes wrong in the hook function its name is printed to the console.

  • Even better than a named function, you can pass a strinG:

    beforeEach('some description', function() {
      // beforeEach:some description
    });

Using done() in hook functions

If your hook function needs to do something asynchronous it can take the done parameter and call it when done, just like tests can:

beforeEach(function(done) {
  db.clear(function(err) {
    if (err) return done(err);
    db.save([tobi, loki, jane], done);
  });
});

When Mocha sees that your hook function takes the done param it'll wait for it to call done() before continuing.

Can you also return a Promise from a hook function? Or use async and await? Probably.

Don't use =>

If you use => then this is lexically bound, so you can't use Mocha's this which has useful things on it like this.timeout().

https://mochajs.org/#arrow-functions

Calling it() without a function registers a pending test

You can call it() and pass a description but no function:

it('should return -1 when the value is not present');

This registers a "pending" test that hasn't been written yet. These are listed as pending (instead of passing or failing) in the console when you run the tests.

If a test does have code you can still mark it as pending by using it.skip() (or describe.skip() or context.skip()).

Don't comment out tests! Use skip() instead.

only() and skip()

Run just one test or suite. Multiple only()s will run multiple tests only. An it.only() overrides a describe.only() that contains the it.only() (other it()s in the describe.only() won't be run).

skip() works the same way but blacklists tests instead of whitelisting them. The blacklisted tests are marked as pending tests.

You can call skip() dynamically:

it('should only test in the correct environment', function() {
  if (/* check test environment */) {
    // make assertions
  } else {
    this.skip();
  }
});

Note: of course, calling this.skip() does not return from the function, any more lines of code after the this.skip() will still be executed (don't do that).

this.skip() can also be called in a before() and will abort all tests in the before()s scope.

Dynamically generating tests - parametrize

You can call describe(), context() and skip() inside a loop. Allows you to do something similar to a pytest parametrize, for example.

[
  {args: [1, 2],       expected: 3},
  {args: [1, 2, 3],    expected: 6},
  {args: [1, 2, 3, 4], expected: 10}
].forEach(function(test) {
  it('correctly adds ' + test.args.length + ' args', function() {
    var res = add.apply(null, test.args);
    assert.equal(res, test.expected);
  });
});

Testing asynchronous code with done(), by returning Promises, or with async / await

If your it() function takes a done parameter then Mocha doesn't mark the test as passed until done() is called (and the test fails if done isn't called before a timeout):

it('should save without error', function(done) {
  var user = new User('Luna');
  user.save(function(err) {
    if (err) done(err);
      else done();
    });
});

Mocha will error if a test calls done() more than once.

You can also just do this (not sure how this works):

it('should save without error', function(done) {
  var user = new User('Luna');
  user.save(done);
});

Returning Promises

Instead of calling done() you can just return a Promise:

it('responds with matching records', function() {
  return db.find({ type: 'User' }).should.eventually.have.length(3);
});

Mocha will error if a test both calls done() and returns a Promise.

Using async / await

If your environment supports it (IE doesn't) you can use JavaScript's new async and await keywords:


it('responds with matching records', async function() {
  const users = await db.find({ type: 'User' });
  users.should.have.length(3);
});

More examples

Lots of example Mocha tests: https://mochajs.org/#examples

node_modules
<html>
<head>
<meta charset="utf-8">
<title>Mocha Browser Example</title>
<link href="https://cdn.rawgit.com/mochajs/mocha/2.2.5/mocha.css" rel="stylesheet" />
</head>
<body>
<div id="mocha"></div>
<script src="https://cdn.rawgit.com/mochajs/mocha/2.2.5/mocha.js"></script>
<script src="http://chaijs.com/chai.js"></script>
<script>
mocha.setup("bdd");
var assert = chai.assert;
describe("my code", function() {
it("does something", function() {
chai.assert.isTrue(true);
});
});
mocha.run();
</script>
</body>
</html>
{
"name": "19e682776b2716283983abda78e6f8aa_mocha",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
"brace-expansion": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
"integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
"dev": true,
"requires": {
"balanced-match": "1.0.0",
"concat-map": "0.0.1"
}
},
"browser-stdout": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz",
"integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=",
"dev": true
},
"commander": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
"integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
"dev": true
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
},
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
},
"diff": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz",
"integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==",
"dev": true
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
"inflight": "1.0.6",
"inherits": "2.0.3",
"minimatch": "3.0.4",
"once": "1.4.0",
"path-is-absolute": "1.0.1"
}
},
"growl": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz",
"integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==",
"dev": true
},
"has-flag": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
"integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=",
"dev": true
},
"he": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
"integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
"dev": true
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": {
"once": "1.4.0",
"wrappy": "1.0.2"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"dev": true
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "1.1.8"
}
},
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
},
"mocha": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-5.0.0.tgz",
"integrity": "sha512-ukB2dF+u4aeJjc6IGtPNnJXfeby5d4ZqySlIBT0OEyva/DrMjVm5HkQxKnHDLKEfEQBsEnwTg9HHhtPHJdTd8w==",
"dev": true,
"requires": {
"browser-stdout": "1.3.0",
"commander": "2.11.0",
"debug": "3.1.0",
"diff": "3.3.1",
"escape-string-regexp": "1.0.5",
"glob": "7.1.2",
"growl": "1.10.3",
"he": "1.1.1",
"mkdirp": "0.5.1",
"supports-color": "4.4.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1.0.2"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
},
"supports-color": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz",
"integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==",
"dev": true,
"requires": {
"has-flag": "2.0.0"
}
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
}
}
}
{
"name": "19e682776b2716283983abda78e6f8aa_mocha",
"version": "1.0.0",
"description": "",
"main": "test.js",
"scripts": {
"test": "mocha"
},
"repository": {
"type": "git",
"url": "git+ssh://[email protected]/19e682776b2716283983abda78e6f8aa.git"
},
"author": "Sean Hammond",
"license": "ISC",
"bugs": {
"url": "https://gist.github.com/19e682776b2716283983abda78e6f8aa"
},
"homepage": "https://gist.github.com/19e682776b2716283983abda78e6f8aa",
"devDependencies": {
"mocha": "^5.0.0"
}
}
let assert = require("assert");
describe("my code", function() {
beforeEach("named funco", function namedFun() {
});
it("pending test");
it("does something", function() {
assert.equal(true, true);
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment