- Mocha allows you to
describe('')
your test suites and run yourit('should do something')
tests within test suites and runs in NodeJS and the browser - Chai allows you to
expect(something).to.be.true
in a very expressive way
yarn add --dev mocha chai sinon
Testing setup
By default, Mocha automatically looks for files with .js
, .mjs
or .cjs
extension inside a directory test
(but not in subfolders), relative to the current working directory.
Mocha can run tests in serial or parallel mode.
A test suite is created using describe()
, they can be nested. A test is created using it()
within a test suite.
Browser testing setup
This article is about testing NodeJS files, but if you want to run browser tests you can quickly initialize a browser-based setup: Running the following will create a index.html
, mocha.css
, mocha.js
and tests.spec.js
:
mocha init .
Testing sync vs async code
Synchronous code
When testing synchronous code, Mocha will automatically continue on to the next test. In this example, we’re using Node.js’ built-in assert
module.
var assert = require('assert'); describe('Array', function () { describe('#indexOf()', function () { it('should return -1 when the value is not present', function () { assert.equal([1, 2, 3].indexOf(4), -1); }); }); });
this
. If you do not need to use Mocha’s context, lambdas should work but refactoring it later might be painful."scripts": { "test": "mocha" }
npm test
Callback-style async code
Call the done()
function to indicate when the async call is done.
describe('User', function () { describe('#save()', function () { it('should save without error', function (done) { var user = new User('Luna'); user.save(function (err) { if (err) done(err); else done(); }); }); }); });
Promise-style async code
The following examples rely on chai-as-promised. To set it up:
yarn add --dev chai-as-promised
const chai = require("chai"); const expect = chai.expect; const chaiAsPromised = require("chai-as-promised"); chai.use(chaiAsPromised);
Expecting a Promise to resolve / return a value
beforeEach(function () { return db.clear().then(function () { return db.save([tobi, loki, jane]); }); }); describe('#find()', function () { it('respond with matching records', function () { return db.find({type: 'User'}).should.eventually.have.length(3); // or without a return use // doSomethingAsync().should.eventually.equal("foo").notify(done); }); });
Expecting a Promise to reject / throw an exception
it("should throw error if no value is given", function () { return expect(thisWillFail()).to.be.rejectedWith("Need string as input value"); // or // expect(thisWillFail()).to.be.rejectedWith("Need string as input value").notify(done); });
async/await-style async code
beforeEach(async function () { await db.clear(); await db.save([tobi, loki, jane]); }); describe('#find()', function () { it('responds with matching records', async function () { const users = await db.find({type: 'User'}); users.should.have.length(3); }); });
Managing test execution
Pending tests
// a pending test it('should return -1 when the value is not present');
Skipping tests
it.skip('should return -1 unless present', function () { // this test will not be run });
Running only specific tests
// an exclusive test describe.only('#indexOf()', function () { // ... }); // or it.only('should return -1 unless present', function () { // ... });
Retrying tests
describe('retries', function () { // Retry all tests in this suite up to 4 times this.retries(4); beforeEach(function () { browser.get('http://www.yahoo.com'); }); it('should succeed on the 3rd try', function () { // Specify this test to only retry up to 2 times this.retries(2); expect($('.foo').isDisplayed()).to.eventually.be.true; }); });
Test timeouts
You can apply timeouts on suite-level, test-level or hook-level, or disable via this.timeout(0)
. This will be inherited by all nested suites and test-cases that do not override the value.
describe('a suite of tests', function () { this.timeout(500); it('should take less than 500ms', function (done) { setTimeout(done, 300); }); it('should take less than 500ms as well', function (done) { setTimeout(done, 250); }); });
Dynamically generating Tests
Testing the same thing but just with different parameters can be easily done by generating tests dynamically with pure JavaScript. The following example even works with “right-click run” features of IDEs, which usually do not work if we used a .forEach
handler instead:
describe('add()', function () { const testAdd = ({args, expected}) => function () { const res = add(args); assert.strictEqual(res, expected); }; it('correctly adds 2 args', testAdd({args: [1, 2], expected: 3})); it('correctly adds 3 args', testAdd({args: [1, 2, 3], expected: 6})); it('correctly adds 4 args', testAdd({args: [1, 2, 3, 4], expected: 10})); });
Lifecycle hooks
A hook simply runs code either once before/after the first or last test or every time before/after each test. Hooks can be sync or async.
before, after, beforeEach, afterEach
describe('hooks', function () { before(function () { // runs once before the first test in this block }); after(function () { // runs once after the last test in this block }); beforeEach(function () { // runs before each test in this block }); afterEach(function () { // runs after each test in this block }); // test cases });
Example of an async hook
You can use one of the async styles (callback, promise, async/await) also for hooks. The following example clears a database and saves three predefined users for each test (fixture):
describe('Connection', function () { var db = new Connection(), tobi = new User('tobi'), loki = new User('loki'), jane = new User('jane'); beforeEach(function (done) { db.clear(function (err) { if (err) return done(err); db.save([tobi, loki, jane], done); }); }); describe('#find()', function () { it('respond with matching records', function (done) { db.find({type: 'User'}, function (err, res) { if (err) return done(err); res.should.have.length(3); done(); }); }); }); });
Hook descriptions
Instead of an anonymous hook function, you can use one of the following to describe your hooks to pinpoint errors in your tests:
beforeEach(function namedFun() { // beforeEach:namedFun }); beforeEach('some description', function () { // beforeEach:some description });
Root hooks
In some cases, you may want a hook before (or after) every test in every file. These are called root hooks. A Root Hook Plugin file is a script which exports (via module.exports
) a mochaHooks
property. The following hook resets the SinonJS sandbox:
const sinon = require("sinon"); exports.mochaHooks = { afterEach() { // Restores the default sandbox after every test sinon.restore(); }, };
Finally, use --require test/hooks.js
or even better, use a config file when running your tests:
module.exports = { "require": "test/hooks.js" }
Global fixtures
Global fixtures are good for spinning up a server, opening a socket, or otherwise creating a resource that your tests will repeatedly access via I/O. Just as with global hooks, global fixtures must either use --require
to be loaded or you define them in a config file.
let server; export const mochaGlobalSetup = async () => { server = await startSomeServer({port: process.env.TEST_PORT}); console.log(`server running on port ${server.port}`); }; export const mochaGlobalTeardown = async () => { await server.stop(); console.log('server stopped!'); };
Then connect to the database in your tests:
import {connect} from 'my-server-connector-thingy'; describe('my API', function () { let connection; before(async function () { connection = await connect({port: process.env.TEST_PORT}); }); it('should be a nice API', function () { // assertions here }); after(async function () { return connection.close(); }); });
Asserting tests with Chai
The reason to use Chai is to replace the Node.js standard assert function. Chai offers your three styles of writing assertions, it’s best to stick to one: assert, expect or should. For the full list of options check the BDD API on chai’s official website.
// two ways to add a custom error message expect(1).to.be.a('string', 'nooo why fail??'); expect(1, 'nooo why fail??').to.be.a('string');
// Asserts that the target is strictly (===) equal to the given val expect(1).to.equal(1); expect('foo').to.equal('foo'); expect('foo').to.be.a('string'); expect({a: 1}).to.be.an('object'); expect(null).to.be.a('null'); expect(null).to.be.null; expect(undefined).to.be.an('undefined'); expect(undefined).to.be.undefined; expect(NaN).to.be.NaN; expect(new Error).to.be.an('error'); expect(Promise.resolve()).to.be.a('promise'); expect(new Float32Array).to.be.a('float32array'); expect(Symbol()).to.be.a('symbol'); expect(true).to.be.true; expect(false).to.be.false; expect([1, 2, 3]).to.be.an('array').that.includes(2); expect([]).to.be.an('array').that.is.empty; expect(function () {}).to.not.throw(); expect({a: 1}).to.not.have.property('b'); expect([1, 2]).to.be.an('array').that.does.not.include(3); // Target object deeply (but not strictly) equals `{a: 1}` expect({a: 1}).to.deep.equal({a: 1}); expect({a: 1}).to.not.equal({a: 1}); // Target array deeply (but not strictly) includes `{a: 1}` expect([{a: 1}]).to.deep.include({a: 1}); expect([{a: 1}]).to.not.include({a: 1}); // Target object deeply (but not strictly) includes `x: {a: 1}` expect({x: {a: 1}}).to.deep.include({x: {a: 1}}); expect({x: {a: 1}}).to.not.include({x: {a: 1}}); // Target array deeply (but not strictly) has member `{a: 1}` expect([{a: 1}]).to.have.deep.members([{a: 1}]); expect([{a: 1}]).to.not.have.members([{a: 1}]); // Target set deeply (but not strictly) has key `{a: 1}` expect(new Set([{a: 1}])).to.have.deep.keys([{a: 1}]); expect(new Set([{a: 1}])).to.not.have.keys([{a: 1}]); // Target object deeply (but not strictly) has property `x: {a: 1}` expect({x: {a: 1}}).to.have.deep.property('x', {a: 1}); expect({x: {a: 1}}).to.not.have.property('x', {a: 1}); expect({a: 1, b: 2}).to.not.have.any.keys('c', 'd'); expect({a: 1, b: 2}).to.have.all.keys('a', 'b');