How to Unit Test with NodeJS?

October 21, 2019

We will use Mocha and Chai to do unit tests and talk about the importance of testing.

In this article, we are going to dive into unit testing and learn about how to use Mocha and Chai to write our tests. We will also take a look at how to do granular testing, an important concept when you have hundreds or thousands of unit tests. We will also talk about why testing is important.

Overview

  • What is unit testing?
  • Mocha — what, how, run, result
  • Chai — what, how, run, result
  • Testing a real NodeJS function — code, test, run, result
  • Granular testing with the Mocha CLI — test a single file, test a group of tests or a single test
  • Testing Opinion — granular testing, testing & code quality
  • Testing Asynchronous functions
  • Review

What is unit testing?

“UNIT TESTING is a level of software testing where individual units/ components of a software are tested. The purpose is to validate that each unit of the software performs as designed. A unit is the smallest testable part of any software. It usually has one or a few inputs and usually a single output. In procedural programming, a unit may be an individual program, function, procedure, etc.” — Software Fundamentals, Unit Testing

If you want to learn more about unit testing at a high level. Definitely, take a look at the link above.

Mocha (a testing framework for NodeJS)

What is Mocha?

Mocha is a testing framework for NodeJS which allows developers to easily test their code. Mocha has a CLI that we talk about below and is full of helpful features. Mocha is one of the most popular NodeJS testing frameworks and as a result, there are lots of tutorials and documentation.

How to install Mocha?

# global install
npm install mocha -g
# project install
npm install mocha --save-dev

How to use Mocha?

  1. create a directory called test at the root of your project
  2. create a test file under tests called example.test.js
  3. paste in the following snippet
const assert = require('assert');
describe('Simple Math Test', () => {
 it('should return 2', () => {
        assert.equal(1 + 1, 2);
    });
 it('should return 9', () => {
        assert.equal(3 * 3, 9);
    });
});
test folder structure

Breakdown of the code snippet

  • describe — a logical grouping of tests, “Simple Math Test”
  • it — a single test, “it.. should return x”
  • assert — how you validate your test works or fails, “assert.equal(1+ 1, 2)”

Update package.json

  • open package.json
  • change the “scripts” block to the following code snippet
"scripts": {
  "test": "mocha"
}

Run test

npm run test

Result

Test output — success

Break tests on purpose

Now let’s adjust test/example.test.js to fail. Update your test to the following code snippet.

const assert = require('assert');
describe('Simple Math Test', () => {
 it('should return 2', () => {
        assert.equal(1 + 1, 2);
    });
 it('should return 9', () => {
        assert.equal(3 * 2, 9);
    });
});

Rerun the test

npm run test

Result

Test output — failure

Chai (testing in English)

What is Chai?

Similar to Mocha assert.equal() we can use Chai to write tests like English sentences.

How to install Chai?

npm install --save-dev chai

How to use Chai?

const expect = require('chai');
...
expect(1 + 1).to.equal(2);
expect(isTrue).to.be.true;

You can also use should all though we won’t get into it in this article.

const should = require('chai').should();
...
isTrue.should.equal(true);

Update our test file

Now we can update our test/example.test.js file with the following code snippet.

const expect = require('chai').expect;
describe('Simple Math Test', () => {
 it('should return 2', () => {
        expect(1 + 1).to.equal(2);
    });
 it('should return 9', () => {
        expect(3 * 3).to.equal(9);
    });
});

Rerun the test

npm run test

Result

Test output — success 🎉

Alright, now let’s do this with a real NodeJS function.

Testing a real NodeJS function

Create a NodeJS function

We are going to make this really simple and not have a nested folder structure. Let’s do the following.

  1. Create a file called math.js at the root of your project
  2. Copy in the following code snippet
const math = {};
math.add = (num1, num2) => num1 + num2;
math.multiply = (num1, num2) => num1 * num2;
module.exports = math;

Create a test file

Now we need a new test file to handle validating that our math.js code is working properly.

  1. Create a file called math.test.js under the testdirectory
  2. Copy the following code snippet
const expect = require('chai').expect;
// import math file
const math = require('../math');
describe('math.js tests', () => {
    describe('math.add() Test', () => {
        it('should equal 2', () => {
            const result = math.add(1, 1);
            expect(result).to.equal(2);
        });
        it('should equal 4', () => {
            const result = math.add(2, 2);
            expect(result).to.equal(4);
        });
    });
    
    describe('math.multiply() Test', () => {
        it('should equal 3', () => {
            const result = math.multiply(3, 1);
            expect(result).to.equal(3);
        });
        it('should equal 10', () => {
            const result = math.multiply(5, 2);
            expect(result).to.equal(10);
        });
    });
});

Notice how we put an outer describe() at the very top. This will affect how the test result looks in the terminal and make it easier to parse.

Rerun the tests

npm run test

Result

If you would like to see the tests fail, try passing a different number!

Granular testing with the Mocha CLI

Test a single file independently

As I’m sure you noticed above, we are seeing both the tests from example.test.js and math.test.js when we run npm run test. What if we want to only test math.test.js?

npm run test /path/to/test

Rerun the test

npm run test test/math.test.js

Result

Test output — single file — success

Test a single set of tests inside a file independently

Now that we know how to test everything and test a single file. What if we want to test a single set of functions inside our desired test file.

Well for this we need to use the Mocha --grepoption. This will allow us to search our tests for a specific string matching the set of tests we want to run. This is made even easier using describe() because it naturally isolates a set of tests.

npm run test -- --grep "searchable string"

Let’s target the math.add() tests only. Since we named our describe() function as describe('math.add() ..') this will be easy.

Rerun the test

The initial -- allows us to reuse our npm run test command that we set up in our package.json and then we use --grep to isolate the tests.

npm run test -- --grep "math.add()"

Result

There we go now we are targeting a specific describe() inside our tests.

Test a single, it, test

Similar to the above we can just copy the --grep command and use a string which equals the single it() test.

Let’s target the first test inside describe('math.add() ..') . This would be the it('should equal 2') . However, note that if we had multiple tests with the same string e.g. should equal 2 we would have some cross-over.

Rerun the test

npm run test -- --grep "should equal 2"

Result

Testing Opinion

Why am I harping on how to test at a granular level?

It will help combat the common objections to testing. From personal experience when a testing suite grows, it becomes a nightmare to understand.

Well, when it comes to testing, most developers don’t care for it as much as writing the code. Some developers don’t care about it at all.

On the surface, testing can slow down development. However, if you think from a more macro-level you can easily see how having a full test suite alongside your code has a massive long term benefit.

For instance, when new developers join your team. When projects run for 6+ months with new code constantly being added.

What does testing encourage?

Testing complex spaghetti code is a nightmare. Having a mindset where tests are a requirement, not a “nice-to-have” will make you as a developer write more modular and reusable code. As you will avoid creating nightmares for yourself by default.

Testing and code quality

Once you have a test suite of any size you can immediately start using the tests to reduce the number of small errors being pushed into production. The tests will act as a baseline for your entire team to work from.

Meaning that prior to every pull request, deployment, or code review. You have the baseline established. My code is passing all tests. This saves time, money, and gives confidence that new code (on the surface) didn’t break old code.

Testing Asynchronous functions

Well, first off let’s talk about the history of promises in JavaScript.

History of Asynchronous JavaScript

I would highly recommend reading this article to get insight into the progression of Asynchronous JavaScript. It does a wonderful job showing code snippets and helping set the stage for why Async/Await is so powerful.

  1. Callbacks — functions get passed a “callback” function and when the called function finishes executing it uses the “callback” to return
  2. Promises — wrapper on functions with a simplified syntax, resolve/reject in the called function to handle success/failure and .then()/.catch() in the outer function to handle the success/failure of the called functions. Easy to chain functions together using multiple .then().then().then().
  3. Async/Await — released with ES8, a simplified syntax that removes some of the overhead with promises. The new syntax async will mark the function as asynchronous. Then you can simply say await function() instead of doing function().then() .

How to test asynchronous code?

Let’s use the math.add() function to test and let’s replace the math.js file with the following code snippet.

const math = {};
// math.add = (num1, num2) => num1 + num2;
math.add = async (num1, num2) => await num1 + num2;
// math.multiply = (num1, num2) => num1 * num2;
math.multiply = (num1, num2) => new Promise((resolve, reject) => {
    resolve(num1 * num2);
});
module.exports = math;

As you can see we kept the previous non-async version and added in the async version below. What we can see is two variants, the math.add() is using traditional promises with resolve/reject and the math.multiply() is using async/await syntax.

Both math.add() and math.multiply() are effectively the same to our test suite. As you see below we can choose to use async/await or .then() syntax inside our tests. My preference is async/await, even if the function we are testing was written using resolve/reject.

Now let’s update our math.test.js file with the following for math.add().

it('should equal 2', async () => {
   const result = await math.add(1, 1);
   expect(result).to.equal(2);
});

You can see we are now using async/await to stop test execution until we get a result from math.add().

Rerun the test

npm run test -- --grep "should equal 2"

Result

Test output — success

Let’s take a look at the same function, but using .then().

it('should equal 2', () => {
    math.add(1, 1).then(result => expect(result).to.equal(2));
});

If we were using callbacks instead of promises we would use the Mocha done() syntax, however, I’m not going to get into that here as that is not how we should be writing code nowadays (IMO).

Rerun the test

npm run test -- --grep "should equal 2"

Result

Test output — success

This is great, we’ve been able to see the similarities between async/await and resolve/reject, but what about when it comes to testing error handling?

How to test asynchronous error handling?

Above, we showed what it looks like when our functions return a successful response. However, we need to also have tests which confirm that our error handling is working properly, e.g. catching errors.

Let’s modify the math.add() function inside themath.js file to throw an error if an argument isn’t passed to the function. We will do this using a try/catch block and async/await.

math.add = async (num1, num2) => {
    try {
        if(num1 && num2) {
            const result = await num1 + num2;
            return result;
        } else {
            throw 'missing arg';
        }
    } catch (err) {
        throw err;
    }
};

Now let’s create a new test which only passes a single argument instead of two numbers. This will play into the new if/else statement which checks to see if both arguments were passed, added above.

it('should throw an error', async () => {
    try {
        await math.add(1);
    } catch (error) {
        expect(error).to.equal('missing ar');
    }
});

As you can see we’ve added a try/catch block that allows us to handle the error and we are checking for the error message. However, we’ve put a typo on purpose.

Rerun the test

npm run test -- --grep "should throw an error"

Result

Test output — failure (wrong error message)

As you can see from the output above our test was looking for the error to be “missing arg”, but we were checking for “missing ar”. Let’s update this.

it('should throw an error', async () => {
    try {
        await math.add(1);
    } catch (error) {
        expect(error).to.equal('missing arg');
    }
});

Now let’s rerun the test.

Rerun the test

npm run test -- --grep "should throw an error"

Result

Test output — success

Alright, that’s all for now. In future (planned) articles we will dive into integration testing and then move into how to write unit tests and integration tests for serverless.

Review

In this article we covered a lot of topics, here is the full list.

  • What is unit testing?
  • Mocha — what, how, run, result
  • Chai — what, how, run, result
  • Testing a real NodeJS function — code, test, run, result
  • Granular testing with the Mocha CLI — test a single file, test a group of tests or a single test
  • Testing Opinion — granular testing, testing & code quality
  • Testing Asynchronous functions — plus, error handling!
  • Review

I hope that you found this useful and I hope this helps to de-mistify some of the gaps I had when first learning how to unit test.

Serverless Handbook
Access free book

The dream team

At Serverless Guru, we're a collective of proactive solution finders. We prioritize genuineness, forward-thinking vision, and above all, we commit to diligently serving our members each and every day.

See open positions

Looking for skilled architects & developers?

Join businesses around the globe that trust our services. Let's start your serverless journey. Get in touch today!
Ryan Jones - Founder
Ryan Jones
Founder
Speak to a Guru
arrow
Edu Marcos - CTO
Edu Marcos
Chief Technology Officer
Speak to a Guru
arrow
Mason Toberny
Mason Toberny
Head of Enterprise Accounts
Speak to a Guru
arrow

Join the Community

Gather, share, and learn about AWS and serverless with enthusiasts worldwide in our open and free community.