Testing in Node.js

Testing is a crucial aspect of software development, ensuring that your code behaves as expected and remains reliable over time. In Node.js, unit testing is a fundamental practice that involves testing individual units or components of your application in isolation. This chapter will guide you through the process of writing unit tests in Node.js, from the basics to more advanced techniques, with a focus on practical examples.

What is Unit Testing?

Unit testing is a software testing technique where individual units or components of the software are tested in isolation from the rest of the application. A “unit” can be a function, method, or object, and the goal of unit testing is to validate that each unit of the software performs as expected.

Example:

Consider a simple function that adds two numbers:

				
					// File: math.js
function add(a, b) {
  return a + b;
}

module.exports = add;

				
			

A unit test for this function would involve checking whether the function correctly adds two numbers.

Why Unit Testing is Important

Unit testing offers several benefits, including:

  • Early Bug Detection: Catching errors early in the development process.
  • Documentation: Tests serve as a form of documentation, describing how functions are supposed to behave.
  • Refactoring Confidence: Enables safe refactoring by ensuring existing functionality isn’t broken.
  • Maintainability: Makes codebases more maintainable by ensuring consistency and reliability.

Setting Up a Testing Environment in Node.js

Installing Mocha and Chai

To start testing in Node.js, you’ll need a testing framework. Mocha is a popular choice for its simplicity and flexibility, and Chai is an assertion library that works well with Mocha.

  1. Initialize Your Project:

    If you haven’t already, create a Node.js project and initialize it:

				
					mkdir nodejs-testing
cd nodejs-testing
npm init -y

				
			

2.Install Mocha and Chai:

Install Mocha and Chai as development dependencies:

				
					npm install --save-dev mocha chai

				
			

3.Project Structure:

Your project should look like this:

				
					nodejs-testing/
├── test/
│   └── math.test.js
├── math.js
└── package.json

				
			

Creating a Basic Test

Let’s create our first test for the add function.

File: test/math.test.js

				
					// File: test/math.test.js
const add = require('../math');
const { expect } = require('chai');

describe('Addition Function', () => {
  it('should add two numbers correctly', () => {
    const result = add(2, 3);
    expect(result).to.equal(5);
  });
});

				
			
  • describe: Defines a test suite, a collection of related tests.
  • it: Defines an individual test.
  • expect: An assertion method provided by Chai to check that the result equals 5.

Running the Test

Add a test script to your package.json:

				
					"scripts": {
  "test": "mocha"
}

				
			

Run the test:

				
					npm test

				
			
				
					// Output 
  Addition Function
    ✓ should add two numbers correctly

  1 passing (10ms)

				
			

Writing Your First Unit Test

Testing a Simple Function

Let’s add more functionality to our math.js file and write tests for it.

File: math.js

				
					// File: math.js
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

module.exports = { add, subtract };

				
			

Update your test file to include tests for the subtract function.

File: test/math.test.js

				
					npm test

				
			
				
					// Output 
  Math Functions
    ✓ should add two numbers correctly
    ✓ should subtract two numbers correctly

  2 passing (10ms)

				
			
				
					// File: test/math.test.js
const { add, subtract } = require('../math');
const { expect } = require('chai');

describe('Math Functions', () => {
  it('should add two numbers correctly', () => {
    const result = add(2, 3);
    expect(result).to.equal(5);
  });

  it('should subtract two numbers correctly', () => {
    const result = subtract(5, 3);
    expect(result).to.equal(2);
  });
});

				
			

Testing Asynchronous Code

Asynchronous code is common in Node.js, and testing it requires a different approach.

Testing Promises

Consider a function that returns a promise.

File: math.js

				
					// File: math.js
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

function multiplyAsync(a, b) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(a * b), 1000);
  });
}

module.exports = { add, subtract, multiplyAsync };

				
			

File: test/math.test.js

				
					// File: test/math.test.js
const { add, subtract, multiplyAsync } = require('../math');
const { expect } = require('chai');

describe('Math Functions', () => {
  it('should add two numbers correctly', () => {
    const result = add(2, 3);
    expect(result).to.equal(5);
  });

  it('should subtract two numbers correctly', () => {
    const result = subtract(5, 3);
    expect(result).to.equal(2);
  });

  it('should multiply two numbers asynchronously', async () => {
    const result = await multiplyAsync(2, 3);
    expect(result).to.equal(6);
  });
});

				
			

Explanation:

  • async/await: Used to handle the asynchronous multiplyAsync function.
  • await: Pauses the test until the promise resolves.
				
					npm test

				
			
				
					// Output 
  Math Functions
    ✓ should add two numbers correctly
    ✓ should subtract two numbers correctly
    ✓ should multiply two numbers asynchronously (1005ms)

  3 passing (1s)

				
			

Testing Callbacks

If you’re dealing with callbacks, you can use done to signal Mocha when the test is complete.

File: math.js

				
					// File: math.js
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

function multiplyCallback(a, b, callback) {
  setTimeout(() => {
    callback(a * b);
  }, 1000);
}

module.exports = { add, subtract, multiplyCallback };

				
			

File: test/math.test.js

				
					// File: test/math.test.js
const { add, subtract, multiplyCallback } = require('../math');
const { expect } = require('chai');

describe('Math Functions', () => {
  it('should add two numbers correctly', () => {
    const result = add(2, 3);
    expect(result).to.equal(5);
  });

  it('should subtract two numbers correctly', () => {
    const result = subtract(5, 3);
    expect(result).to.equal(2);
  });

  it('should multiply two numbers using callback', (done) => {
    multiplyCallback(2, 3, (result) => {
      expect(result).to.equal(6);
      done();
    });
  });
});

				
			

Explanation:

  • done: A callback provided by Mocha that signals the end of the test. You must call it to indicate the test has finished.
				
					npm test

				
			
				
					// Output 
  Math Functions
    ✓ should add two numbers correctly
    ✓ should subtract two numbers correctly
    ✓ should multiply two numbers using callback (1005ms)

  3 passing (1s)

				
			

Using Test Hooks

Mocha provides hooks that allow you to run code before and after your tests.

Before and After Hooks

Use before and after hooks to set up or clean up resources.

File: test/math.test.js

				
					// File: test/math.test.js
const { add, subtract, multiplyCallback } = require('../math');
const { expect } = require('chai');

describe('Math Functions', () => {
  let testValue;

  before(() => {
    testValue = 10; // Initialize test data
  });

  after(() => {
    testValue = null; // Clean up test data
  });

  it('should add two numbers correctly', () => {
    const result = add(2, 3);
    expect(result).to.equal(5);
  });

  it('should subtract two numbers correctly', () => {
    const result = subtract(5, 3);
    expect(result).to.equal(2);
  });

  it('should multiply two numbers using callback', (done) => {
    multiplyCallback(2, 3, (result) => {
      expect(result).to.equal(6);
      done();
    });
  });
});

				
			

BeforeEach and AfterEach Hooks

These hooks run before and after each test in the suite.

File: test/math.test.js

				
					// File: test/math.test.js
const { add, subtract, multiplyCallback } = require('../math');
const { expect } = require('chai');

describe('Math Functions', () => {
  let a, b;

  beforeEach(() => {
    a = 2;
    b = 3;
  });

  afterEach(() => {
    a = 0;
    b = 0;
  });

  it('should add two numbers correctly', () => {
    const result = add(a, b);
    expect(result).to.equal(5);
  });

  it('should subtract two numbers correctly', () => {
    const result = subtract(5, 3);
    expect(result).to.equal(2);
  });

  it('should multiply two numbers using callback', (done) => {
    multiplyCallback(a, b, (result) => {
      expect(result).to.equal(6);
      done();
    });
  });
});

				
			

Explanation:

  • beforeEach: Initializes a and b before each test.
  • afterEach: Resets a and b after each test.

Advanced Testing Techniques

Mocking and Stubbing with Sinon

Sinon is a library used for creating spies, mocks, and stubs, allowing you to test code that depends on external services.

Install Sinon:

				
					npm install --save-dev sinon

				
			

File: test/math.test.js

				
					// File: test/math.test.js
const sinon = require('sinon');
const { multiplyAsync } = require('../math');
const { expect } = require('chai');

describe('Math Functions', () => {
  it('should multiply two numbers asynchronously', async () => {
    const stub = sinon.stub().resolves(6);
    const result = await stub(2, 3);
    expect(result).to.equal(6);
  });
});

				
			

Explanation:

  • sinon.stub: Replaces the original function with a test-specific function.

Testing HTTP Requests with Supertest

Supertest is a library used for testing HTTP requests.

Install Supertest:

				
					npm install --save-dev supertest

				
			

File: server.js

				
					// File: server.js
const express = require('express');
const app = express();

app.get('/add', (req, res) => {
  const { a, b } = req.query;
  const result = Number(a) + Number(b);
  res.json({ result });
});

module.exports = app;

				
			

File: test/server.test.js

				
					// File: test/server.test.js
const request = require('supertest');
const app = require('../server');
const { expect } = require('chai');

describe('GET /add', () => {
  it('should add two numbers via query params', async () => {
    const response = await request(app).get('/add').query({ a: 2, b: 3 });
    expect(response.body.result).to.equal(5);
  });
});

				
			

Explanation:

  • request(app).get(‘/add’): Sends a GET request to the /add route.
  • .query({ a: 2, b: 3 }): Sends query parameters a and b with the request.
				
					npm test

				
			
				
					// Output 
  GET /add
    ✓ should add two numbers via query params

  1 passing (10ms)

				
			

Code Coverage

Measuring Code Coverage with Istanbul (nyc)

Code coverage measures how much of your code is being tested. nyc is a popular tool for this purpose.

Install nyc:

				
					npm install --save-dev nyc

				
			

Update your package.json:

				
					"scripts": {
  "test": "nyc mocha"
}

				
			

Run the tests with coverage:

				
					npm test

				
			
				
					// Output 
=============================== Coverage summary ===============================
Statements   : 100% ( 8/8 )
Branches     : 100% ( 2/2 )
Functions    : 100% ( 3/3 )
Lines        : 100% ( 8/8 )
================================================================================

				
			

Interpreting Code Coverage Reports

  • Statements: Percentage of code statements executed.
  • Branches: Percentage of conditional branches executed.
  • Functions: Percentage of functions executed.
  • Lines: Percentage of lines executed.

Best Practices for Unit Testing

Writing Maintainable Tests

  • Keep Tests Small: Test only one thing per test.
  • Use Descriptive Names: Clearly describe what the test is checking.
  • Avoid External Dependencies: Test units in isolation.

Organizing Your Test Files

  • Mirror Your Source Structure: Place test files in the test folder, mirroring the structure of your source files.
  • Use Naming Conventions: Use .test.js as a suffix for test files.
				
					project-root/
├── src/
│   └── math.js
└── test/
    └── math.test.js

				
			

Unit testing in Node.js is an essential practice that ensures your code is robust, maintainable, and reliable. By following the techniques and best practices outlined in this chapter, you can write effective unit tests that cover a wide range of scenarios, from simple functions to complex asynchronous code and external dependencies.Happy coding !❤️

Table of Contents

Contact here

Copyright © 2025 Diginode

Made with ❤️ in India