Introduction
Writing unit tests is something beginners and seasoned engineers alike typically put off for later phases of development, yet - they're key to stable and robust software development.
The basic premise of test-driven development (TDD) is writing your tests even before you start coding. That's a great goal to strive for, but it takes a lot of discipline and planning when you're trying to follow its principles! To make this whole process a lot easier, you can resort to easy-to-use and powerful testing and assertion frameworks, such as Mocha and Chai.
In this article, we'll start out by introducing you to these two libraries and then show you how to use them together to quickly create readable and functional unit tests.
Chai
Chai is an assertion library that provides both the BDD (behavior-driven development) and TDD (test-driven development) styles of programming for testing code, and is meant to be paired with a testing library that lets you organize tests. It's very commonly paired with Mocha.
It has three main APIs:
should
expect
assert
// should interface (BDD-style)
var.should.equal(var2)
// expect interface (BDD-style)
expect.var.to.be.equal(var2)
// assert interface (TDD-style)
assert.equal(var1, var2)
Throughout this article, we'll focus on the BDD style using Chai's
expect
interface, though using other interfaces/styles as per your own intuition is perfectly okay. Theassert
interface is the most common TDD assertion framework.
expect
uses a very natural language API to write your assertions, which will make your tests easier to write and improve upon later on down the road. This is done by chaining together getters to create and execute the assertion, making it easier to translate requirements into code:
let user = {name: 'Scott'};
// Requirement: The object 'user' should have the property 'name'
expect(user).to.have.property('name');
Note: See how you can pretty much read the assertion in a natural language and understand what's going on? That's one of the main advantages of using an assertion library like Chai!
A few more examples of these getters are:
to
be
is
and
has
have
Quite a few of these getters can be chained together and used with assertion methods like true
, ok
, exist
, and empty
to create some complex assertions in just one line:
"use strict";
const expect = require('chai').expect;
// Simple assertions
expect({}).to.exist;
expect(26).to.equal(26);
expect(false).to.be.false;
expect('hello').to.be.string;
// Modifiers ('not')
expect([1, 2, 3]).to.not.be.empty;
// Complex chains
expect([1, 2, 3]).to.have.length.of.at.least(3);
Note: A full list of the available methods can be found here.
You might also want to check out the list of available plugins for Chai. These make it much easier to test more complex features.
Take chai-http for example, which is a plugin that helps you test server routes:
"use strict";
const chai = require('chai');
const chaiHttp = require('chai-http');
chai.use(chaiHttp);
chai.request(app)
.put('/api/auth')
.send({username: '[email protected]', password: 'abc123'})
.end(function(err, res) {
expect(err).to.be.null;
expect(res).to.have.status(200);
});
Organizing Test Cases with Mocha - describe() and it()
Mocha is a testing framework for Node that gives you the flexibility to run asynchronous (or synchronous) code serially. Any uncaught exceptions are shown alongside the test case in which it was thrown, making it easy to identify exactly what failed and why.
It's advised to install Mocha globally:
$ npm install mocha -g
You'll want it to be a global install since the mocha
command is used to run the tests for the project in your local directory.
What does this piece of code do?
it()
should return X. it()
defines test cases, and Mocha will run each it()
as a unit test. To organize multiple unit tests, we can describe()
a common functionality, and thus structure Mocha tests.
This is probably best described with a concrete example:
"use strict";
const expect = require('chai').expect;
describe('Math', function() {
describe('#abs()', function() {
it('should return positive value of given negative number', function() {
expect(Math.abs(-5)).to.be.equal(5);
});
});
});
In the describe()
method, we defined a test name, called #abs()
. You can individually run tests by their name as well - this'll be covered later.
Note: With Mocha tests, you don't need to require()
any of the Mocha methods. These methods are provided globally when run with the mocha
command.
In order to run these tests, save your file and use the mocha
command:
$ mocha .
Math
#abs()
✓ should return positive value of given number
1 passing (9ms)
The output is a breakdown of the tests that ran and their results. Notice how the nested describe()
calls carry over to the results output. It's useful to have all of the tests for a given method or feature nested together.
These methods are the basis for the Mocha testing framework. Use them to compose and organize your tests however you like. We'll see one example of this in the next section.
Writing Tests With Mocha and Chai
The recommended way to organize your tests within your project is to put all of them in their own /test
directory. By default, Mocha checks for unit tests using the globs ./test/*.js
and ./test/*.coffee
. From there, it will load and execute any file that calls the describe()
method.
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
It's common to suffix the test files with .test.js
for the source files that contain Mocha tests:
├── package.json
├── lib
│ ├── db.js
│ ├── models.js
│ └── util.js
└── test
├── db.test.js
├── models.test.js
├── util.test.js
└── util.js
util.js
in the test
directory wouldn't contain any unit tests, just utility functions to help with testing.
Note: You can use whatever structure makes sense to you, the unit tests are automatically picked up.
When it comes to actually writing the tests, it helps to organize them using the describe()
methods. You can organize them by feature, function, file, or any other arbitrary level. For example, a test file organized to describe the workings at function level looks like:
"use strict";
const expect = require('chai').expect;
describe('Math', function() {
describe('#abs()', function() {
it('should return positive value of given negative number', function() {
expect(Math.abs(-5)).to.be.equal(5);
});
it('should return positive value of given positive number', function() {
expect(Math.abs(3)).to.be.equal(3);
});
it('should return 0 given 0', function() {
expect(Math.abs(0)).to.be.equal(0);
});
});
});
Running the tests would then give you the output:
$ mocha .
Math
#abs()
✓ should return positive value of given negative number
✓ should return positive value of given positive number
✓ should return 0 given 0
3 passing (11ms)
Expanding even further, you might even have tests for multiple methods in a single file. In this case, the methods are grouped by the Math
object:
"use strict";
const expect = require('chai').expect;
describe('Math', function() {
describe('#abs()', function() {
it('should return positive value of given negative number', function() {
expect(Math.abs(-5)).to.be.equal(5);
});
it('should return positive value of given positive number', function() {
expect(Math.abs(3)).to.be.equal(3);
});
it('should return 0 given 0', function() {
expect(Math.abs(0)).to.be.equal(0);
});
});
describe('#sqrt()', function() {
it('should return the square root of a given positive number', function() {
expect(Math.sqrt(25)).to.be.equal(5);
});
it('should return NaN for a given negative number', function() {
expect(Math.sqrt(-9)).to.be.NaN;
});
it('should return 0 given 0', function() {
expect(Math.sqrt(0)).to.be.equal(0);
});
});
});
Which results in:
$ mocha .
Math
#abs()
✓ should return positive value of given negative number
✓ should return positive value of given positive number
✓ should return 0 given 0
#sqrt()
✓ should return the square root of a given positive number
✓ should return NaN for a given negative number
✓ should return 0 given 0
6 passing (10ms)
Mocha Hooks - before(), after(), beforeEach() and afterEach()
Admittedly, most unit tests aren't this simple. A lot of times you'll probably need other resources to perform your tests, like a database, or some other external resource (or a mock/stub of them). In order to set this up, we can use one or more of the following Mocha hook methods:
before()
: Runs before all tests in the given blockbeforeEach()
: Runs before each test in the given blockafter()
: Runs after all tests in the given blockafterEach()
: Runs after each test in the given block
These hooks are the perfect place for performing setup and teardown work required for your tests. One of the common use cases is to establish a connection to your database before running the tests:
"use strict";
const expect = require('chai').expect;
let User = require('../models').User;
describe('Users', function() {
let database = null;
before(function(done) {
// Connect to DB or create test table
});
afterEach(function(done) {
// Undo changes to DB or drop test table
});
describe('#save()', function() {
it('should save User data to database', function(done) {
// Use your database here...
});
});
describe('#load()', function() {
it('should load User data from database', function(done) {
// Use your database here...
});
});
});
Before any of the tests are run, the function sent to our before()
method is run (and only run once throughout the tests), which establishes a connection to the database. Once this is done, our test suites are then run.
Since we wouldn't want the data from one test suite to affect our other tests, we need to clear the data from our database after each suite is run. This is what afterEach()
is for. We use this hook to clear all of the database data after each test case is run, so we can start from a clean slate for the next tests.
Running Mocha Tests
For the majority of cases, this part is pretty simple. Assuming you've already installed Mocha and navigated to the project directory, most projects just need to use the mocha
command with no arguments to run their tests:
$ mocha
Math
#abs()
✓ should return positive value of given negative number
✓ should return positive value of given positive number
✓ should return 0 given 0
#sqrt()
✓ should return the square root of a given positive number
✓ should return NaN for a given negative number
✓ should return 0 given 0
6 passing (10ms)
This is slightly different from our previous examples since we didn't need to tell Mocha where our tests were located. In this example, the test code is in the expected location of /test
.
There are, however, some helpful options you may want to use when running tests. If some of your tests are failing, for example, you probably don't want to run the entire suite every time you make a change. For some projects, the full test suite could take a few minutes to complete. That's a lot of wasted time if you really only need to run one test.
For cases like this, you should tell Mocha which tests to run. This can be done using the
-g <pattern>
or-f <sub-string>
options.
To run individual tests, you can supply the -g
flag and add a common pattern between the tests you want to run. For example, if you want to run the #sqrt()
tests:
$ mocha -g sqrt
Math
#sqrt()
✓ should return the square root of a given positive number
✓ should return NaN for a given negative number
✓ should return 0 given 0
3 passing (10ms)
Notice that the #abs()
tests were not included in this run. If you plan accordingly with your test names, this option can be utilized to only run specific sections of your tests.
These aren't the only useful options, however. Here are a few more options for Mocha that you might want to check out:
--invert
: Inverts-g
and-f
matches--recursive
: Include sub-directories--harmony
: Enable all harmony features in Node
Note: You can check out the full list of options by using the mocha -h
command, or on this page.
Conclusion
Keep in mind that both Mocha and Chai can be used for testing just about any type of Node project, whether it's a library, command-line tool, or even a website. Utilizing the various options and plugins available to you, you should be able to satisfy your testing needs pretty easily. Each of these libraries is very useful for validating your code and should be used in just about all of your Node projects.
Hopefully, this has served as a useful introduction to Mocha and Chai. There is a lot more to learn than what I've presented here, so be sure to check out the docs for more info.