DEV Community

Leslie Gyamfi
Leslie Gyamfi

Posted on

Unit Testing with Mocha & Chai 🚀

Unit testing, also known as module testing or component testing is a form of software testing and a software development practice where small units or pieces of code (a line of code, a method, or a class) are tested independently. The end goal of unit testing is to ensure that each unit functions as expected and that bugs are caught at the early stages of the development cycle.

Regardless of the type of application you're creating, testing should always come before release. There are many different testing frameworks available, some of which include Mocha, Jasmine, and Cypress. But in this extensive guide, we’re going to take a look at unit testing with Mocha and Chai.

Why Mocha and Chai?

Mocha is a popular fun, simple, and flexible JavaScript test framework for Node.js environments. It offers a clean and readable syntax for defining test suites and cases, making writing and maintaining tests easy. It also has support for asynchronous testing.

Chai is a flexible assertion library used together with Mocha that provides different assertion styles (expect, should, assert) and a rich set of methods for verifying expected outcomes within your tests. It can be used as a BDD/TDD assertion library for NodeJS and with any other JavaScript testing framework. How does Chai do this?

To begin with, BDD and TDD (Behavior-Driven Development and Test-Driven Development) are software development methodologies that emphasize writing tests before or alongside writing the actual code.

In TDD, you start by writing a failing test that defines the expected behavior of a code unit. Then, you write the code to make the test pass. This ensures your code is designed to fulfill specific requirements from the beginning.

BDD focuses on describing the desired behavior of the system from the user's perspective. This is typically done using a Gherkin syntax with "Given-When-Then" statements. While BDD can be used with TDD, it's not strictly required.

So Chai’s is used as an assertion library for BDD and TDD within your BDD/TDD workflows. While Chai itself isn't inherently tied to BDD or TDD, it provides the tools to write assertions that align with these methodologies.

Chai also has several interfaces that a developer can choose from and it will look like the tests are being written in English sentences.

Benefits of using Mocha and Chai for Unit Testing in Node.js

  1. Readable and expressive assertions
    Chai offers various assertion styles and expressive syntax choices, smoothly integrating with Mocha. You may select the assertion style that best fits your preferences and readability requirements from various available styles, including the widely used expect, assert, and should.

  2. Support for asynchronous testing
    Node.js applications frequently use asynchronous processes like database queries and network requests. You may write tests involving asynchronous code with Mocha without the need for extra libraries or complicated configurations since it smoothly manages asynchronous testing.

Running Mocha Tests in Node

First, install Mocha as a dependency by running the command below on the terminal of your computer:

npm i --save-dev mocha

Open the package.json file and change the scripts block to mocha as seen in the code below:

{
 "name": "unit-tests-mocha-chai",
 "version": "1.0.0",
 "description": "Unit Test",
 "main": "index.js",
 "directories": {
   "test": "tests"
 },
 "scripts": {
   "test": "mocha"
 },
 "author": "LambdaTest-Leslie",
 "license": "ISC",
 "devDependencies": {
   "mocha": "^10.4.0"
 }
}
Enter fullscreen mode Exit fullscreen mode

Our script is now configured to run a test using mocha. Next, create a test folder in your project's root directory. With this configuration, you can simply run this command to run tests in your project:

npm test

Writing Tests with Mocha and Chai

Writing tests with Mocha mostly requires the use of an assertion library which will be used to verify that the result from an operation aligns with the expected result.

We’ll use Chai as the assertion library in this blog. Run the command below to install Chai as a development dependency:
npm i --save-dev chai

The package.json file should now look like this after Mocha and Chai have both been installed:


"name": "unit-tests-mocha-chai",
 "version": "1.0.0",
 "description": "Unit Test",
 "main": "index.js",
 "directories": {
   "test": "tests"
 },
 "scripts": {
   "test": "mocha"
 },
 "author": "LambdaTest-Leslie",
 "license": "ISC",
 "devDependencies": {
   "chai": "^5.1.1",
   "mocha": "^10.4.0"
 }
}
Enter fullscreen mode Exit fullscreen mode

Chai provides the following assert, expect, and should styles. To learn more about the assertion styles, visit the official Chai documentation.

var assert = require('chai').assert
  , foo = 'bar'
  , beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };

assert.typeOf(foo, 'string'); // without optional message
assert.typeOf(foo, 'string', 'foo is a string'); // with optional message
assert.equal(foo, 'bar', 'foo equal `bar`');
assert.lengthOf(foo, 3, 'foo`s value has a length of 3');
assert.lengthOf(beverages.tea, 3, 'beverages has 3 types of tea');
Enter fullscreen mode Exit fullscreen mode
var expect = require('chai').expect
  , foo = 'bar'
  , beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };

expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(foo).to.have.lengthOf(3);
expect(beverages).to.have.property('tea').with.lengthOf(3);

Enter fullscreen mode Exit fullscreen mode
var should = require('chai').should() //actually call the function
  , foo = 'bar'
  , beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };

foo.should.be.a('string');
foo.should.equal('bar');
foo.should.have.lengthOf(3);
beverages.should.have.property('tea').with.lengthOf(3);

Enter fullscreen mode Exit fullscreen mode

Chai provides numerous methods for verifying different aspects of test results. Here are some commonly used ones:

  1. assert.equal(a, b): Checks if two values are strictly equal.
  2. expect(result).to.be.true: Verifies if a value is true.
  3. assert.typeOf(value, 'string'): Asserts the data type of a variable.

How to Write Test Suites with Mocha

To start writing test suites with Mocha, specify your test suite with the expected functionalities for the test. In your root project directory, and the test folder, create a test.app.js file. Next, add the following code to it:

const assert = require("assert");
const { add } = require("../src/app");

describe("Add the numbers", () => {
 it("should return the sum of two numbers", () => {
   const result = add(1, 2);
   assert.equal(result, 3);
 });
});
Enter fullscreen mode Exit fullscreen mode

Next, implement the add function's functionality as a module export, and run the tests. In the root directory of your project, create a new app.js file in the src folder, and add the following code:

const add = (a, b) => a + b;

module.exports = {
 add,
};
Enter fullscreen mode Exit fullscreen mode

Now, run the tests in your terminal using the script defined in the package.json file:

npm test

This is how the output should be:

Image description

You can see in the image above that the test passed and we got a green checkmark because the output from the add function was equal to 3. But if we change the assertion and say that we expect the result to be 4 and run the test, the test is going to fail:

Image description

This is the basis of all unit tests: you run a code and then you get to check that the result you’re expecting actually corresponds with the result that the function produces.

Remember that the test we just wrote was for synchronously executed codes. But since most JavaScript applications involve a lot of synchronous code, we must look into how to test asynchronous codes too.

Testing Asynchronous Code

Testing asynchronous (async) code is important in modern JavaScript (Nodejs) applications and Mocha provides built-in support for testing asynchronous operations using:

  • callback functions
  • promises or
  • async/await environments

Let’s look at each of these built-in support methods in the following section.

Testing Async Code Using Callback Functions

In JavaScript, a callback is a function passed into another function as an argument, which is then invoked inside the outer function to complete some kind of routine or action. Callbacks are a fundamental part of asynchronous programming in JavaScript.

For example, consider a fetchData function that simulates fetching data from a database with a delay. Create a new async.js file in the root directory of your project and add the following code:

// async.js
function fetchData(callback) {
   setTimeout(() => {
     callback('data');
   }, 1000);
 }
  module.exports = fetchData;

Enter fullscreen mode Exit fullscreen mode

When testing functions that use callbacks, we need to inform Mocha when the asynchronous operation is complete by using the done callback. This tells Mocha to wait until the done callback is called before considering the test complete.

The following code contains a test for this asynchronous function using a callback function. Create a new async.test.js file in the test directory of your project and add the following code:

// Use dynamic import to load chai
import('chai').then(chai => {
   const expect = chai.expect;
    // Assuming fetchData is a function that fetches data asynchronously using a callback
   const fetchData = require('../async');
    describe('Asynchronous Test with Callback', function() {
     it('should fetch data correctly', function(done) {
       fetchData((data) => {
         expect(data).to.equal('data');
         done();  // Indicates that the test is complete
       });
     });
   });
 }).catch(error => {
   console.error('Error importing Chai:', error);
 });
Enter fullscreen mode Exit fullscreen mode

In this test:

  • We import the fetchData function and the chai assertion library.
  • We define a test using it().
  • Inside the test, we call fetchData and pass a callback function.
  • In the callback, we use Chai's expect to assert that the fetched data is 'data'.
  • We call done to signal Mocha that the test is finished. Run the tests in your terminal and you should get something like this:

Image description

You realize a test coverage table in our output shows all test files that have passed. Test coverage is a metric used to measure the amount of code being tested by your test suite. A higher coverage usually indicates a better-tested code.

To get the test coverage metric to run through your tests, run the command below to install it:

npm install nyc --save-dev

Next, update the test script in your package.json file:

{
 "name": "unit-tests-mocha-chai",
 "version": "1.0.0",
 "description": "Unit Test",
 "main": "index.js",
 "directories": {
   "test": "tests"
 },
 "scripts": {
   "test": "nyc mocha"
 },
 "author": "LambdaTest-Leslie",
 "license": "ISC",
 "devDependencies": {
   "chai": "^5.1.1",
   "mocha": "^10.4.0"
 },
 "dependencies": {
   "nyc": "^15.1.0"
 }
}

Enter fullscreen mode Exit fullscreen mode

Now you can run your tests and you will get a summary showing the percentage of statements, branches, functions, and lines covered by your tests.

Testing Async Code Using Promises

Another method of testing asynchronous code is by using promises. In JavaScript, a promise represents the eventual completion (or failure) of an asynchronous operation and its resulting value. It provides a cleaner and more robust way to handle asynchronous operations compared to callbacks.

To test asynchronous code that uses promises, we’ll create a file that is based on promises. Create a new promise.js file in the root directory of your project and add the following code:

// promise.js
function fetchData() {
   return new Promise((resolve) => {
     setTimeout(() => {
       resolve('data');
     }, 1000);
   });
 }
  module.exports = fetchData;

Enter fullscreen mode Exit fullscreen mode

Mocha natively supports promises. If a returned promise is resolved, Mocha will consider the test successful. If it is rejected, Mocha will consider the test failed.

Now, the following code contains a simple test suite for the promise method. Create a new promise.test.js file in the test directory of your project and add the following code:

// test/promise.test.js
import("chai").then((chai) => {
 const expect = chai.expect;

 const fetchData = require("../promise");

 describe("Asynchronous Test with Promise", function () {
   it("should fetch data correctly", function () {
     return fetchData().then((data) => {
       expect(data).to.equal("data");
     });
   });
 });
});
Enter fullscreen mode Exit fullscreen mode

In this test:

  • We call fetchData, which returns a promise.
  • We use the then method to handle the resolved value.
  • Inside the then method, we use Chai's expect to assert that the fetched data is 'data'.
  • Now, go ahead and run the test with this command: npm test

The result should look like this:

Image description

Testing Async Code Using Async or Await Environments

Async/await is syntactic sugar built on top of promises, making asynchronous code look and behave more like synchronous code. An async function always returns a promise. The await keyword is used to wait for a promise to resolve or reject.

For environments that support the more recent async/await syntax, Mocha also supports passing async functions as the second argument to it().

Let's update our fetchData function to use async/await.

async function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('data');
    }, 1000);
  });
}
module.exports = fetchData;

Enter fullscreen mode Exit fullscreen mode

Create a new asyncawait.test.js file in the test directory of your project and add the following code:

// test/asyncawait.test.js
import("chai").then((chai) => {
 const expect = chai.expect;

 const fetchData = require("../asyncawait");

 describe("Asynchronous Test with Async/Await", function () {
   it("should fetch data correctly", async function () {
     const data = await fetchData();
     expect(data).to.equal("data");
   });
 });
});
Enter fullscreen mode Exit fullscreen mode

In this test:

  • We define the test function with async.
  • Inside the test, we use the await keyword to wait for the promise returned by fetchData to resolve.
  • We use Chai's expect to assert that the fetched data is 'data'.

Go ahead and run the test. The output should look like this:

Image description

Using Test Hooks with Mocha

Test hooks in Mocha allow you to set up the preconditions for your tests and clean up afterward. These hooks help ensure that each test runs in a controlled environment, making tests more reliable and easier to maintain. Mocha provides several hooks:

  • before(): Runs once before all tests in the suite.
  • after(): Runs once after all tests in the suite.
  • beforeEach(): Runs before each test in the suite.
  • afterEach(): Runs after each test in the suite.

It should be noted that depending on the hooks that apply to a given test suite, the hooks are run together with the tests in the suite in a definite sequence:
before() -> beforeEach() -> test() -> afterEach() -> after()

For example, let's create a simple user management module with functions to add and get users. We'll use Mocha's hooks to set up and tear down test conditions.

Create a user.js file and add the code that follows:

// user.js
class UserManager {
  constructor() {
    this.users = [];
  }

  addUser(user) {
    this.users.push(user);
  }

  getUser(id) {
    return this.users.find(user => user.id === id);
  }

  clearUsers() {
    this.users = [];
  }
}

module.exports = UserManager;
Enter fullscreen mode Exit fullscreen mode

It's time to write a test Suite with Mocha hooks. Create a user.test.js file in the test folder with the following code:

// test/user.test.js
import("chai").then((chai) => {
 const expect = chai.expect;

 const UserManager = require("../user");

 describe("UserManager", function () {
   let userManager;

   // Runs once before all tests in the suite
   before(function () {
     userManager = new UserManager();
   });

   // Runs before each test in the suite
   beforeEach(function () {
     userManager.clearUsers();
   });

   // Runs after each test in the suite
   afterEach(function () {
     // You can perform additional cleanup here if needed
   });

   // Runs once after all tests in the suite
   after(function () {
     userManager = null;
   });

   it("should add a user", function () {
     userManager.addUser({ id: 1, name: "Carie Levin" });
     const user = userManager.getUser(1);
     expect(user).to.deep.equal({ id: 1, name: "Carie Levin" });
   });

   it("should get a user by id", function () {
     userManager.addUser({ id: 2, name: "Janelle Coop" });
     const user = userManager.getUser(2);
     expect(user).to.deep.equal({ id: 2, name: "Janelle Coop" });
   });
 });
});

Enter fullscreen mode Exit fullscreen mode

In our test suite:

  • Before Hook (before): The before hook runs once before all the tests in the suite. It initializes the userManager instance, ensuring a fresh instance is available for all tests.
  • Before Each Hook (beforeEach): The beforeEach hook runs before each test in the suite. It clears the userManager users array before each test to ensure no leftover state from previous tests.
  • After Each Hook (afterEach): The afterEach hook runs after each test in the suite. You can use this hook for additional cleanup, though it is not strictly necessary in our scenario.
  • After Hook (after): The after hook runs once after all tests in the suite. It clears the userManager instance, helping to release resources or perform final cleanup.
  • Tests: The it blocks define individual tests. Each test adds a user to userManager and checks that the user can be retrieved correctly.

Now, run the test. The output should look like this:

Image description

Handling Slow Tests and Timeouts in Mocha

Handling slow tests and timeouts in Mocha is important to ensure that your test suite runs efficiently and provides useful feedback when tests take longer than expected. Mocha provides several mechanisms to manage these aspects, including setting timeouts, marking tests as slow, and handling asynchronous operations properly.

Setting Timeouts

By default, Mocha sets a timeout of 2000 milliseconds (2 seconds) for each test. If a test takes longer than this to complete, it will fail. You can adjust this timeout for individual tests, suites, or globally.

Setting Timeout for Individual Tests

You can set a custom timeout for an individual test using the this.timeout() method within a test function.

// test/timeout.test.js
import("chai").then((chai) => {
 const expect = chai.expect;

describe('Timeout Test', function() {
  it('should complete within 5 seconds', function(done) {
    this.timeout(5000); // Set timeout to 5000 ms (5 seconds)

    setTimeout(() => {
      expect(true).to.be.true;
      done();
    }, 4000); // Simulate an async operation taking 4 seconds
  });
});
});
Enter fullscreen mode Exit fullscreen mode

Setting Timeout for a Suite

You can set a custom timeout for an entire suite using the this.timeout() method within a suite function.

// test/suiteTimeout.test.js
import("chai").then((chai) => {
 const expect = chai.expect;

describe('Suite Timeout Test', function() {
  this.timeout(10000); // Set timeout to 10000 ms (10 seconds) for the entire suite

  it('should complete within 5 seconds', function(done) {
    setTimeout(() => {
      expect(true).to.be.true;
      done();
    }, 4000);
  });

  it('should complete within 8 seconds', function(done) {
    setTimeout(() => {
      expect(true).to.be.true;
      done();
    }, 7000);
  });
});
});


Enter fullscreen mode Exit fullscreen mode

You can also set it programmatically in your test setup file (e.g., test/setup.js).

// test/setup.js
mocha.setup({
  timeout: 10000 // Set global timeout to 10000 ms (10 seconds)
});

Enter fullscreen mode Exit fullscreen mode

Handling Slow Tests

Mocha allows you to mark tests as slow, which can help you identify potentially problematic tests that might need optimization. By default, Mocha considers tests taking longer than 75 milliseconds as slow, but this threshold can be adjusted.

Setting Slow Threshold for Individual Tests
You can set a custom slow threshold for an individual test using the this.slow() method within a test function.

// test/slow.test.js
import("chai").then((chai) => {
 const expect = chai.expect;

describe('Slow Test', function() {
  it('should be marked as slow if it takes more than 2 seconds', function(done) {
    this.slow(2000); // Set slow threshold to 2000 ms (2 seconds)

    setTimeout(() => {
      expect(true).to.be.true;
      done();
    }, 1500); // Simulate an async operation taking 1.5 seconds
  });
});
});
Enter fullscreen mode Exit fullscreen mode

Setting Slow Threshold for a Suite

You can set a custom slow threshold for an entire suite using the this.slow() method within a suite function.

// test/suiteSlow.test.js
import("chai").then((chai) => {
 const expect = chai.expect;

describe('Suite Slow Test', function() {
  this.slow(5000); // Set slow threshold to 5000 ms (5 seconds) for the entire suite

  it('should not be marked as slow', function(done) {
    setTimeout(() => {
      expect(true).to.be.true;
      done();
    }, 3000); // Simulate an async operation taking 3 seconds
  });

  it('should be marked as slow', function(done) {
    setTimeout(() => {
      expect(true).to.be.true;
      done();
    }, 6000); // Simulate an async operation taking 6 seconds
  });
});
});


Enter fullscreen mode Exit fullscreen mode

Best Practices for Performing Unit Tests with Mocha and Chai

  • Each test should cover a single behavior or function to ensure that failures are easily traceable.

  • Test names should clearly describe the behavior being tested for easier understanding and debugging.

  • Use Mocha's hooks (before, after, beforeEach, afterEach) to set up the necessary environment and clean up after tests.

  • Use variables and configuration files to manage test data, making tests more maintainable.

  • Ensure that you cover edge cases and not just the happy paths to make your tests more robust.

Conclusion

Mastering Mocha and Chai for unit testing is an invaluable skill for any JavaScript developer. These tools offer a robust framework for writing, running, and managing tests, ensuring your code is reliable and maintainable.

You can fine-tune your testing environment to suit your specific needs by understanding how to handle asynchronous tests with callbacks, promises, and async/await, and by utilizing Mocha’s powerful CLI options.

Implementing best practices such as managing timeouts, identifying slow tests, and setting up proper test hooks will further enhance your testing efficiency. With Mocha and Chai, you can confidently develop high-quality, bug-free JavaScript applications.

Do let us know me you leverage unit testing with mocha and chai in your applications!

Top comments (0)