Introduction to Node.js API Testing with Mocha and Chai

on

Unit testing is the procedure during which individual parts of a program are tested to determine whether they work as expected.

Whether you plan to follow a test-driven development (TDD) approach, or simply want to verify that your program works each time you make a bunch of changes, it is generally accepted that unit testing is a good practice.

What unit testing is not, is a substitute for writing good code — you’ll hardly ever get 100% test coverage, so a passing test does not necessarily mean that your code is bug-free.

In this tutorial, we’ll be creating a simple REST API in Node.js Express and use Mocha with the Chai assertion library to verify that the API works as expected.

Create an Express REST API

If you have not setup your Node.js development environment yet, start by following the instructions in my previous tutorial.

We’ll be using Node.js v20 and enable ECMAScript modules (ESM) mode.

node -v
v20.7.0

This will be our project’s directory structure:

.
├── index.js
├── package.json
├── app
│   └── app.js
└── test
    └── app.test.js

Our API will have a single endpoint /colors, which we can use to GET a list of colors and POST (insert) new values.

In the app directory, create a file app.js to write our Express application:

import express from "express";

const DEFAULT_COLORS = ["RED", "GREEN", "BLUE"];

const app = express();

app.use(express.json());

/**
 * Array holding color values
 */
const colors = [...DEFAULT_COLORS];

/**
 * Returns a list of colors
 * Response body (JSON): {results: [....]}
 */
app.get("/colors", (req, res, next) => {
  res.json({
    results: colors,
  });
});

/**
 * Inserts new color in the 'colors' array
 * Request body (JSON): {color: ...}
 */
app.post("/colors", (req, res, next) => {
  if (req.is("application/json") && typeof req.body.color === "string") {
    const color = req.body.color.trim().toUpperCase();

    if (color && !colors.includes(color)) {
      colors.push(color);

      // 201 Created
      res.status(201).send({
        results: colors,
      });

      return;
    }
  }

  res.status(400).send(); // 400 Bad Request
});

/**
 * Export our Express app
 */
export default app;

Assuming you are fairly familiar with Express, the above code should be easy to follow.

The reason we export the Express application in the last line is because we want to be able to automatically launch our server during testing and import our app instance into Chai HTTP.

For starting our server manually, create a file index.js in the root directory.

import app from "./app/app.js"; // Our app

const PORT = 8080;
const HOST = "localhost";

app.listen(PORT, HOST, () => {
  console.log("Listening on %s:%d...", HOST || "*", PORT);
});

Testing with Mocha and Chai

To create our tests, we will be using Mocha, Chai, and Chai HTTP modules.

Mocha is a popular JavaScript test framework — it runs test cases and reports any errors encountered during the process. However, Mocha does not have a built-in assertion library. That’s where Chai comes in, which is an assertion library for Node.js. Assertions are simple statements that are always expected to evaluate to true, and if not, they throw an error.

Furthermore, since we are testing API endpoints, we also need a module for sending HTTP requests. Chai-http fulfills that role by communicating with the application/server, returning the responses, and using Chai assertions to verify the results.

In a Mocha test, we describe our tests using the describe function, which typically contains our test cases. The individual test cases are implemented using the it function, which is where we insert our assertions.

describe("API endpoint /colors", () => {
  ...
  it("should return all colors", () => {
    ...
  });
  ...
});

Create a new directory test and inside that directory a file app.test.js to write our Mocha test.

import chai, { expect } from "chai";
import chaiHttp from "chai-http";
import app from "../app/app.js"; // Our app

chai.use(chaiHttp);

describe("API endpoint /colors", () => {
  before(() => {});

  after(() => {});

  // GET - List all colors
  it("should return all colors", async () => {
    const res = await chai.request(app).get("/colors");
    expect(res).to.have.status(200);
    expect(res).to.be.json;
    expect(res.body).to.be.an("object");
    expect(res.body.results).to.be.an("array");
  });

  // GET - Invalid path
  it("should return Not Found", async () => {
    const res = await chai.request(app).get("/INVALID_PATH");
    expect(res).to.have.status(404);
  });

  // POST - Add new color
  it("should add new color", async () => {
    const res = await chai.request(app).post("/colors").send({
      color: "YELLOW",
    });
    expect(res).to.have.status(201);
    expect(res).to.be.json;
    expect(res.body).to.be.an("object");
    expect(res.body.results).to.be.an("array").that.includes("YELLOW");
  });

  // POST - Bad Request
  it("should return Bad Request", async () => {
    const res = await chai.request(app).post("/colors").type("form").send({
      color: "YELLOW",
    });
    expect(res).to.have.status(400);
  });
});

Chai HTTP sends all requests to our Express application instance, which we import in Line 3, and returns a promise (Lines 14, 23, 29, 40). Mocha waits for the promises inside the it functions to resolve, before it continues to the next test case, unless it is setup to use callbacks. The before and after blocks can be used to setup our tests before any testing begins, and clean-up after all tests have been completed.

Running the tests

Before we run our tests, we should first install the application dependencies. Since we only use Mocha/Chai during development, it’s a good practice to save them as devDependencies using the --save-dev parameter.

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

Your package.json should be similar to the one below.

{
  "name": "colors-api",
  "version": "1.0.0",
  "private": true,
  "description": "",
  "type": "module",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "test": "mocha --recursive --timeout 5000"
  },
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "chai": "^4.3.10",
    "chai-http": "^4.4.0",
    "mocha": "^10.2.0"
  }
}

Note that, by default, mocha --recursive runs all tests it finds under the test directory. In addition, npm automatically inserts node_modules/.bin into our shell’s PATH, that’s why we can execute Mocha without installing it globally.

To run our test, simply execute npm test.

npm test
API endpoint /colors
  ✓ should return all colors
  ✓ should return Not Found
  ✓ should add new color
  ✓ should return Bad Request
4 passing (71ms)

If there are no errors, then the output should be similar to the above, otherwise Mocha will report the test case and code line where the test failed on.

You can find all source code in this tutorial in my GitHub repository.