Mocha Chai Notes

Mocha Chai Quick Reference

Mocha Chai Notes

by John Vincent


Posted on June 1, 2017


Put in one place those pesky Mocha/Chai options.

This stuff ends up sprayed everywhere, so let's create a reference document.

Mocha

  • Mocha is a test framework.
  • Chai is an assertion library.
  • Faker fakes variable values.

Install for Node

npm init
npm install --save-dev mocha
npm install --save-dev chai
npm install --save-dev chai-http
npm install --save-dev faker
Add to package.json

  "scripts": {
    "start": "node main.js",
    "test": "mocha",
    "test-2": "ENV_1=value1 ENV_2=true mocha"
  },

and then

npm start

npm test

Basic Code

main.js

const Game = require("./game.js");
const Board = require("./board.js");

const board = new Board();
const game = new Game(board);

game.start();

board.js

class Board {
    constructor() {
        this.init();
    }

/*
* initial setup of the board model
*/
    init() {
        this.rows = [];
        for (var x = 0; x < 7; x++) {
            var row = [];
            for (var y = 0; y < 7; y++) {
                var legal = true;
                var occupied = true;
                if (!this.isLegal(x, y)) {
                    legal = false;
                    occupied = false;
                }
                if (x === 3 && y === 3) {
                    occupied = false;
                }
                row.push({ legal: legal, occupied: occupied });
            }
            this.rows.push(row);
        }
    }

    /*
* empty the board model
*/
    empty_board() {
        this.rows = [];
        for (var x = 0; x < 7; x++) {
            var row = [];
            for (var y = 0; y < 7; y++) {
                var legal = true;
                var occupied = false;
                if (!this.isLegal(x, y)) {
                    legal = false;
                }
                row.push({ legal: legal, occupied: occupied });
            }
            this.rows.push(row);
        }
    }

    /*
* The board is treated as a square, so the function is used to determine which squares are
* within the board
*/
    isLegal(row, column) {
        if (row < 0 || row > 6) {
            return false;
        }
        if (column < 0 || column > 6) {
            return false;
        }
        if (row === 0 || row === 1 || row === 5 || row === 6) {
            if (column === 0 || column === 1 || column === 5 || column === 6) {
                return false;
            }
        }
        return true;
    }

    isOccupied(row, column) {
        if (!this.isLegal(row, column)) {
            throw Error(`Exception in isOccupied(); row ${row} column ${column} is not legal`);
        }
        return this.rows[row][column].occupied;
    }

    setEmpty(row, column) {
        if (!this.isLegal(row, column)) {
            throw Error(`Exception in setEmpty(); row ${row} column ${column} is not legal`);
        }
        this.rows[row][column].occupied = false;
    }

    setOccupied(row, column) {
        if (!this.isLegal(row, column)) {
            throw Error(`Exception in setEmpty(); row ${row} column ${column} is not legal`);
        }
        this.rows[row][column].occupied = true;
    }

    /*
* Look for any legal move from (row, column)
*
* Return:
*    true => >= 1 legal move from (row, column) was found.
*    false => there are no legal moves from (row, column)
*/
    //    anyLegalFromMoves(row, column) {
    //        return this.isFromUpMoveLegal(row, column) ||
    //                this.isFromDownMoveLegal(row, column) ||
    //                this.isFromLeftMoveLegal(row, column) ||
    //                this.isFromRightMoveLegal(row, column);
    //    }

    makeMoveStatus(status, from_row, from_column, via_row, via_column, to_row, to_column, type) {
        return {
            status: status,
            from: { row: from_row, column: from_column },
            via: { row: via_row, column: via_column },
            to: { row: to_row, column: to_column },
            type: type,
        };
    }

    findMove(row, column, type) {
        for (let current = type; current < 5; current++) {
            if (current === 1) {
                if (this.isFromUpMoveLegal(row, column)) {
                    return this.makeMoveStatus("OK", row, column, row - 1, column, row - 2, column, current);
                }
            } else if (current === 2) {
                if (this.isFromRightMoveLegal(row, column)) {
                    return this.makeMoveStatus("OK", row, column, row, column + 1, row, column + 2, current);
                }
            } else if (current === 3) {
                if (this.isFromDownMoveLegal(row, column)) {
                    return this.makeMoveStatus("OK", row, column, row + 1, column, row + 2, column, current);
                }
            } else if (current === 4) {
                if (this.isFromLeftMoveLegal(row, column)) {
                    return this.makeMoveStatus("OK", row, column, row, column - 1, row, column - 2, current);
                }
            }
        }
        return { status: "None" };
    }

    /*
* 1. Verify from and to are the 2 tiles apart, on a straight line.
* 2. Calculate the tile between from & to.
* 3. Verify from tile is legal and is occupied.
* 4. Verify in between tile is legal and is occupied.
* 5. Verify to tile is legal and is not occupied.
* 6. Update data model:
* 6a. Set from tile to empty
* 6b. Set in between tile to empty
* 6c. Set to tile to occupied.
*/
    makeMove(move) {
        //        console.log(">>> makeMove; move "+JSON.stringify(move));
        let { status, from, to, via } = move;
        if (status !== "OK") {
            throw Error(`Exception in makeMove(); move ${move} is invalid status`);
        }

        if (!this.isLegal(from.row, from.column) || !this.isOccupied(from.row, from.column)) {
            // 3
            throw Error(`Exception in makeMove(); from in ${move} is invalid`);
        }
        if (!this.isLegal(via.row, via.column) || !this.isOccupied(via.row, via.column)) {
            // 4
            throw Error(`Exception in makeMove(); via in ${move} is invalid`);
        }
        if (!this.isLegal(to.row, to.column) || this.isOccupied(to.row, to.column)) {
            // 5
            throw Error(`Exception in makeMove(); to in ${move} is invalid`);
        }

        this.setEmpty(from.row, from.column); // 6a
        this.setEmpty(via.row, via.column); // 6b
        this.setOccupied(to.row, to.column); // 6c

        //        console.log("<<< makeMove; move "+JSON.stringify(move));
        return true;
    }

    deleteMove(move) {
        //        console.log(">>> deleteMove; move "+JSON.stringify(move));
        let { status, from, to, via } = move;
        if (status !== "OK") {
            throw Error(`Exception in deleteMove(); move ${move} is invalid status`);
        }
        this.setOccupied(from.row, from.column);
        this.setOccupied(via.row, via.column);
        this.setEmpty(to.row, to.column);
        //        console.log("<<< deleteMove; move "+JSON.stringify(move));
    }

    isFromUpMoveLegal(row, column) {
        return (
            this.isLegal(row, column) &&
            this.isOccupied(row, column) &&
            this.isLegal(row - 1, column) &&
            this.isOccupied(row - 1, column) &&
            this.isLegal(row - 2, column) &&
            !this.isOccupied(row - 2, column)
        );
    }
    isFromRightMoveLegal(row, column) {
        return (
            this.isLegal(row, column) &&
            this.isOccupied(row, column) &&
            this.isLegal(row, column + 1) &&
            this.isOccupied(row, column + 1) &&
            this.isLegal(row, column + 2) &&
            !this.isOccupied(row, column + 2)
        );
    }
    isFromDownMoveLegal(row, column) {
        return (
            this.isLegal(row, column) &&
            this.isOccupied(row, column) &&
            this.isLegal(row + 1, column) &&
            this.isOccupied(row + 1, column) &&
            this.isLegal(row + 2, column) &&
            !this.isOccupied(row + 2, column)
        );
    }
    isFromLeftMoveLegal(row, column) {
        return (
            this.isLegal(row, column) &&
            this.isOccupied(row, column) &&
            this.isLegal(row, column - 1) &&
            this.isOccupied(row, column - 1) &&
            this.isLegal(row, column - 2) &&
            !this.isOccupied(row, column - 2)
        );
    }
}

module.exports = Board;

Basic Tests

Test files go in folder test

For example, test-board.js

const Board = require("../board");

require("chai").should();

var expect = require("chai").expect;

describe("test Board.isLegal()", function() {
    it("isLegal() should return boolean", function() {
        let board = new Board();
        let ans = board.isLegal(-1, -1);
        ans.should.be.a("boolean");
        ans.should.equal(false);
    });

    it("isLegal() should understand legal tiles", function() {
        let board = new Board();

        for (let row = -3; row < 10; row++) {
            for (let col = -5; col < 11; col++) {
                if (row < 0 || row > 6 || col < 0 || col > 6) {
                    board.isLegal(row, col).should.equal(false);
                    continue;
                }
                if (row === 0 || row === 1 || row === 5 || row === 6) {
                    if (col === 0 || col === 1 || col === 5 || col === 6) {
                        board.isLegal(row, col).should.equal(false);
                        continue;
                    }
                }
                board.isLegal(row, col).should.equal(true);
            }
        }
    });
});

describe("testBoard.isOccupied()", function() {
    it("isOccupied(-1, -1) should throw Error", function() {
        expect(function() {
            new Board().isOccupied(-1, -1);
        }).to.throw(Error);
    });

    it("isOccupied(-1, 3) should throw Error", function() {
        expect(function() {
            new Board().isOccupied(-1, 3);
        }).to.throw(Error);
    });

    it("isOccupied(3, -1) should throw Error", function() {
        expect(function() {
            new Board().isOccupied(3, -1);
        }).to.throw(Error);
    });

    it("isOccupied(3, -1) should not throw Error", function() {
        expect(function() {
            new Board().isOccupied(3, 3);
        }).to.not.throw(Error);
    });

    it("isOccupied(3, 3) of a legal tile should return boolean", function() {
        new Board().isOccupied(3, 3).should.be.a("boolean");
    });

    it("isOccupied() should understand initial setup", function() {
        let board = new Board();

        for (let row = -3; row < 10; row++) {
            for (let col = -5; col < 11; col++) {
                if (row < 0 || row > 6 || col < 0 || col > 6) {
                    continue;
                }
                if (row === 0 || row === 1 || row === 5 || row === 6) {
                    if (col === 0 || col === 1 || col === 5 || col === 6) {
                        continue;
                    }
                }
                if (row === 3 && col === 3) {
                    board.isOccupied(row, col).should.equal(false);
                } else {
                    board.isOccupied(row, col).should.equal(true);
                }
            }
        }
    });
});

describe("testBoard.setEmpty()", function() {
    it("setEmpty(-1, -1) should throw Error", function() {
        expect(function() {
            new Board().setEmpty(-1, -1);
        }).to.throw(Error);
    });

    it("setEmpty(-1, 3) should throw Error", function() {
        expect(function() {
            new Board().setEmpty(-1, 3);
        }).to.throw(Error);
    });

    it("setEmpty(3, -1) should throw Error", function() {
        expect(function() {
            new Board().setEmpty(3, -1);
        }).to.throw(Error);
    });

    it("setEmpty(3, -1) should not throw Error", function() {
        expect(function() {
            new Board().setEmpty(3, 3);
        }).to.not.throw(Error);
    });

    it("setEmpty(2, 3) of a legal tile should return boolean", function() {
        new Board().isOccupied(2, 3).should.be.a("boolean");
    });

    it("setEmpty(2, 3) of an occupied tile should return empty", function() {
        let board = new Board();
        board.isOccupied(2, 3).should.equal(true);
        board.setEmpty(2, 3);
        board.isOccupied(2, 3).should.equal(false);
    });

    it("setEmpty() should understand initial setup", function() {
        let board = new Board();

        for (let row = -3; row < 10; row++) {
            for (let col = -5; col < 11; col++) {
                if (row < 0 || row > 6 || col < 0 || col > 6) {
                    continue;
                }
                if (row === 0 || row === 1 || row === 5 || row === 6) {
                    if (col === 0 || col === 1 || col === 5 || col === 6) {
                        continue;
                    }
                }
                board.setEmpty(row, col);
                board.isOccupied(row, col).should.equal(false);
            }
        }
    });
});

describe("testBoard.setOccupied()", function() {
    it("setOccupied(-1, -1) should throw Error", function() {
        expect(function() {
            new Board().setOccupied(-1, -1);
        }).to.throw(Error);
    });

    it("setOccupied(-1, 3) should throw Error", function() {
        expect(function() {
            new Board().setOccupied(-1, 3);
        }).to.throw(Error);
    });

    it("setOccupied(3, -1) should throw Error", function() {
        expect(function() {
            new Board().setOccupied(3, -1);
        }).to.throw(Error);
    });

    it("setOccupied(3, -1) should not throw Error", function() {
        expect(function() {
            new Board().setOccupied(3, 3);
        }).to.not.throw(Error);
    });

    it("setOccupied(3, 3) of an occupied tile should return occupied", function() {
        let board = new Board();
        board.isOccupied(3, 3).should.equal(false);
        board.setOccupied(3, 3);
        board.isOccupied(3, 3).should.equal(true);
    });

    it("setEmpty() should understand initial setup", function() {
        let board = new Board();

        for (let row = -3; row < 10; row++) {
            for (let col = -5; col < 11; col++) {
                if (row < 0 || row > 6 || col < 0 || col > 6) {
                    continue;
                }
                if (row === 0 || row === 1 || row === 5 || row === 6) {
                    if (col === 0 || col === 1 || col === 5 || col === 6) {
                        continue;
                    }
                }
                board.setEmpty(row, col);
                board.isOccupied(row, col).should.equal(false);
            }
        }
    });
});

Install for Server

npm install --save-dev mocha
npm install --save-dev chai
npm install --save-dev chai-http
npm install --save-dev faker

Note that faker is not required, but I use it so frequently that I always just add it.

Other changes to package.json

"scripts": {
  "start": "nodemon server.js",
  "test": "mocha ./test",
  "devtool-app": "devtool server.js",
  "devtool-test": "devtool ./node_modules/mocha/bin/_mocha ./test"
},

Note, if deploying to Heroku, do not use nodemon

  "scripts": {
      "start": "node server.js",
      "test": "mocha ./test",
      "devtool-app": "devtool server.js",
      "devtool-test": "devtool ./node_modules/mocha/bin/_mocha ./test"
  },

Heroku does not load the environment before starting mocha, thus

"test": "MY-KEY=value mocha ./test"

To run all tests in sub-folders

"test": "mocha --recursive ./test"

Run

Mocha recursively looks for .js files in a test folder

npm test

To setup Mocha tests in Visual Studio Code, please see my notes

Set Order of Mocha Tests

package.json

"test": "mocha ./test"

Include the tests in the order you want to run them, for example

test/test-all.js

require('./app/test-app');
require('./base/test-1');
require('./search/test-search');
  • test/utils/data.js contains test data as Json.
  • test/utils/check.js verifies the data structures are correct.

As a best practice, use the following pattern to describe each test

describe('test-countArticles; Test SubscriptionUtils::countArticles', function() {
  • test-countArticles; file name containing the tests
  • Test SubscriptionUtils::countArticles; what is being tested.

Select Tests

Run tests in a describe/it block

describe.only
it.only

Skip a block/it

describe.skip
it.skip

Code

retrieveUserSubscription(url, userSubscriptions) {
    return url && userSubscriptions && userSubscriptions.length ?
        userSubscriptions.find(test => test.url === url)
        : undefined;
}

which will return undefined for unexpected conditions.

The test case could just check for undefined.

An alternative is to throw an exception.

Exceptions

For example:

class SubscriptionUtils {

retrieveSubscription(url, allSubscriptions) {
    if (url && allSubscriptions && allSubscriptions.length) {
        return allSubscriptions.find(test => test.url === url);
    }
    throw Error('Exception in SubscriptionUtils::retrieveSubscription');
  }
}

test code:

expect(function() { subscriptionUtils.retrieveSubscription(url, null); }).to.throw('Exception in SubscriptionUtils::retrieveSubscription');
expect(function() { subscriptionUtils.retrieveSubscription(null, _subscriptions); }).to.throw('Exception in SubscriptionUtils::retrieveSubscription');
expect(function() { subscriptionUtils.retrieveSubscription(null, null); }).to.throw('Exception in SubscriptionUtils::retrieveSubscription');

expect(function() { subscriptionUtils.retrieveSubscription(url, undefined); }).to.throw('Exception in SubscriptionUtils::retrieveSubscription');
expect(function() { subscriptionUtils.retrieveSubscription(undefined, _subscriptions); }).to.throw('Exception in SubscriptionUtils::retrieveSubscription');
expect(function() { subscriptionUtils.retrieveSubscription(undefined, undefined); }).to.throw('Exception in SubscriptionUtils::retrieveSubscription');
try {}
catch(e) {
    e.should.be.an.instanceof(Error);
}
badInputs.forEach(function(input) {
  (function() {
      adder(input[0], input[1]);
  }).should.throw(Error);

Should

require('chai').should();

Common code usage

a.should.equal(1);
a.should.equal('abc');
ans.should.equal(false);
ans.should.equal(true);
a.should.be.false;
a.should.be.true;

res.body.id.should.be.null;
res.body.id.should.not.be.null;
sub_2.should.be.empty;
sub_1.should.not.be.empty;

res.should.be.html;

res.should.have.status(200);
res.body.length.should.be.at.least(1);
res.body.should.have.length.of(count);
res.should.have.status(200);
res.should.be.json;
res.body.should.be.a('array');

item.should.be.a('object');
item.should.include.keys(expectedKeys);

item.author.should.equal(`${blog.author.firstName} ${blog.author.lastName}`);

res.body.ingredients.should.include.members(newItem.ingredients);

res.body.should.deep.equal(Object.assign(newItem, {id: res.body.id}));
res.body.should.deep.equal(updateData);
 
(new Board()).isOccupied(3, 3).should.be.a('boolean');

Expect

require('chai').expect();
expect(user.username).to.be.null;
expect(user.username).not.to.be.null;

expect(sub).to.be.defined;
expect(sub).be.undefined;

expect(user).to.be.an('object');
expect(sub_1).to.be.an('array');

expect(sub_1_2.title).to.be.equal('A');
expect(saved.length).to.be.equal(2);

expect(item.link).to.have.length.above(30);
expect(item.title).to.have.length.above(20);

Assert

some day...

assert = require('assert')

Avoid Time Outs

it('test long running test', () => {

}).timeout(150000);