Node.js Module Testing with Chai, Sinon, and Mocha Framework

Grzegorz Wziątek

We have updated this text for you!
Update date: 26.12.2024
Author of the update: Adam Olszewski

Introduction

In this article, I will guide you through writing unit tests for Node.js modules and their dependencies. Unit testing is critical for ensuring that small changes in your code don’t break the application. A failure in a key system could lead to significant consequences, as illustrated in the scenario of an airport baggage system failing, which could lead to significant customer dissatisfaction, lawsuits, and financial penalties. By writing effective unit tests, integration tests, and end-to-end tests, we reduce the risk of such failures.
It’s essential to write meaningful tests, not just those that improve the coverage percentage. Good tests help to prevent deploying broken code into production, ensuring that the app behaves as expected even after code changes. In my experience, well-structured unit tests are a lifesaver, helping prevent disasters when deploying changes

Getting Started

Let’s start by creating a project directory and installing the required libraries. These include testing frameworks Mocha, Chai, and Sinon. Mocha is the test runner, Chai provides an assertion library, and Sinon allows for mocking and stubbing dependencies.

Initialize the project
Create a new directory and initialize package.json:
bash

mkdir node-test-tutorial
cd node-test-tutorial
npm init -y

Install dependencies
Install the necessary libraries for testing:
bash

npm install mocha chai sinon node-fetch @babel/core @babel/cli @babel/preset-env --save-dev

This command installs Mocha, Chai, Sinon, Babel for ES6+ support, and node-fetch (to mock the weather API in tests).Configure package.json
Edit the scripts section in package.json to include the following for running the app and tests:
json

"scripts": {
  "start": "babel-node app/app.js --presets @babel/preset-env",
  "test": "mocha --require @babel/register tests/**/*.spec.js"
}

Now, you can run npm start to launch your application and npm test to run your tests.

Writing the Application Code

Let’s create a small app that fetches weather data from an external API (OpenWeatherMap) and returns appropriate messages based on the temperature.api.js (responsible for fetching weather data):
javascript

import fetch from 'node-fetch';

/**
 * Fetches weather data for a given city.
 * @param {string} city The city name
 * @returns {Promise<object>} The weather data
 */
export async function getWeatherData(city) {
    const url = `http://api.openweathermap.org/data/2.5/weather?appid=YOUR_API_KEY&units=metric&q=${encodeURIComponent(city)}`;
    try {
        const response = await fetch(url);
        if (!response.ok) throw new Error('API Error');
        return await response.json();
    } catch (error) {
        throw new Error('Network or API Error');
    }
}

app.js (main app logic using the weather data):
javascript

import { getWeatherData } from './api';
export const TEMPERATURE_FREEZING = 'Temperature is below 0, remember to take a coat and scarf!';
export const TEMPERATURE_COLD = 'Temperature is below 10, remember to take a coat!';
export const TEMPERATURE_OK = 'Temperature is okay today.';
export const TEMPERATURE_HOT = 'Warning! Temperature is above 30 degrees, watch for sunburn!';
export const TEMPERATURE_ERROR = 'API Error. Unable to retrieve temperature.';
export const NO_CITY = 'No city specified.';

/**
 * Returns a weather-related message based on the temperature.
 * @param {number} temperature The current temperature in Celsius
 * @returns {string} The appropriate message
 */
export const getTemperatureMessage = (temperature) => {
    if (temperature === undefined) return TEMPERATURE_ERROR;
    if (temperature < 0) return TEMPERATURE_FREEZING;
    if (temperature < 10) return TEMPERATURE_COLD;
    if (temperature < 30) return TEMPERATURE_OK;
    return TEMPERATURE_HOT;
};

/**
 * Fetches weather data for a specified city and returns a temperature message.
 * @param {string} city The city name
 * @returns {Promise<string>} The temperature-related message
 */
export const getWeatherInfo = async (city) => {
    if (!city) return NO_CITY;
    try {
        const weather = await getWeatherData(city);
        return getTemperatureMessage(weather.main.temp);
    } catch (error) {
        return TEMPERATURE_ERROR;
    }
};

Writing Tests

Now, let’s create tests for the methods in app.js. We’ll use Mocha as the test runner, Chai for assertions, and Sinon for mocking the external API calls.

Test for getTemperatureMessage (covers different temperature ranges):
javascript

import * as app from '../app/app';
const { expect } = require('chai');

describe('App.js Tests', () => {
    describe('Check Temperature Messages', () => {
        it('Temperature not specified', () => {
            const message = app.getTemperatureMessage();
            expect(message).to.equal(app.TEMPERATURE_ERROR);
        });

        it('Temperature below 0°C', () => {
            const message = app.getTemperatureMessage(-5);
            expect(message).to.equal(app.TEMPERATURE_FREEZING);
        });

        it('Temperature between 0°C and 10°C', () => {
            const message = app.getTemperatureMessage(5);
            expect(message).to.equal(app.TEMPERATURE_COLD);
        });

        it('Temperature between 10°C and 30°C', () => {
            const message = app.getTemperatureMessage(20);
            expect(message).to.equal(app.TEMPERATURE_OK);
        });

        it('Temperature above 30°C', () => {
            const message = app.getTemperatureMessage(35);
            expect(message).to.equal(app.TEMPERATURE_HOT);
        });
    });
});

Test for getWeatherInfo with Sinon Stubbing (mocking the external API):
javascript
import * as app from '../app/app';
import * as api from '../app/api';
const { expect } = require('chai');
const sinon = require('sinon');

describe('getWeatherInfo', () => {
    let weatherApiCallStub;

    beforeEach(() => {
        const fakeResponse = { main: { temp: 15 } };
        weatherApiCallStub = sinon.stub(api, 'getWeatherData').callsFake(() => Promise.resolve(fakeResponse));
    });

    afterEach(() => {
        weatherApiCallStub.restore();
    });

    it('should return the correct temperature message', async () => {
        const message = await app.getWeatherInfo('London');
        expect(message).to.equal(app.TEMPERATURE_OK);
        sinon.assert.calledOnce(weatherApiCallStub);
    });

    it('should handle API errors gracefully', async () => {
        weatherApiCallStub.callsFake(() => Promise.reject(new Error('API Error')));
        const message = await app.getWeatherInfo('InvalidCity');
        expect(message).to.equal(app.TEMPERATURE_ERROR);
    });

    it('should handle no city specified', async () => {
        const message = await app.getWeatherInfo();
        expect(message).to.equal(app.NO_CITY);
        sinon.assert.notCalled(weatherApiCallStub);
    });
});

Summary

With the combination of Mocha, Chai, and Sinon, we have successfully written unit tests for our Node.js app. We tested different scenarios, including handling external API responses with Sinon stubbing, ensuring that the application behaves correctly under various conditions.

Good unit tests can prevent catastrophic errors during development and deployment. They make it easier to manage code changes and ensure that your application continues to meet the business requirements. Remember: imperfect tests that run frequently are better than perfect tests that are never written.

Happy coding, and happy testing!

Meet the geek-tastic people, and allow us to amaze you with what it's like to work with j‑labs!

Contact us