Node.js Testing with Jest

Testing is the practice of writing code that automatically verifies that other code works correctly. Without tests, every change to an application requires manual testing — which is slow, error-prone, and does not scale. Automated tests catch bugs early, provide confidence when making changes, and serve as documentation of how code is expected to behave.

Jest is a popular, zero-configuration JavaScript testing framework created by Facebook. It works seamlessly with Node.js applications and provides everything needed to write and run tests: a test runner, assertion library, mocking tools, and code coverage reporting — all in one package.

Types of Tests

  • Unit Tests: Test individual functions or modules in isolation.
  • Integration Tests: Test how multiple parts of the application work together (e.g., a route handler with a database).
  • End-to-End (E2E) Tests: Test the full user flow through the entire application.

This topic focuses on unit and integration testing — the most common in Node.js development.

Installing Jest

npm install jest --save-dev

Add a test script to package.json:

"scripts": {
  "test": "jest",
  "test:watch": "jest --watch",
  "test:coverage": "jest --coverage"
}

Writing the First Test

The Function to Test – math.js

// math.js

function add(a, b) {
  return a + b;
}

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

function multiply(a, b) {
  return a * b;
}

function divide(a, b) {
  if (b === 0) throw new Error("Cannot divide by zero");
  return a / b;
}

module.exports = { add, subtract, multiply, divide };

The Test File – math.test.js

Jest automatically discovers test files with the .test.js or .spec.js suffix, or files inside a __tests__ folder.

// math.test.js

const { add, subtract, multiply, divide } = require('./math');

describe('Math Functions', function() {

  test('add() should return the sum of two numbers', function() {
    expect(add(3, 4)).toBe(7);
    expect(add(-1, 1)).toBe(0);
    expect(add(0, 0)).toBe(0);
  });

  test('subtract() should return the difference of two numbers', function() {
    expect(subtract(10, 4)).toBe(6);
    expect(subtract(0, 5)).toBe(-5);
  });

  test('multiply() should return the product of two numbers', function() {
    expect(multiply(3, 5)).toBe(15);
    expect(multiply(-2, 4)).toBe(-8);
  });

  test('divide() should return the quotient of two numbers', function() {
    expect(divide(10, 2)).toBe(5);
    expect(divide(9, 3)).toBe(3);
  });

  test('divide() should throw an error when dividing by zero', function() {
    expect(() => divide(10, 0)).toThrow("Cannot divide by zero");
  });

});

Run tests:

npm test

Understanding the Test Structure

  • describe() — Groups related tests together under a label. It is optional but improves readability.
  • test() or it() — Defines a single test case with a description and a function.
  • expect(value) — Creates an assertion about a value.
  • Matcher — A method chained onto expect() that checks a condition (e.g., .toBe(), .toEqual()).

Common Jest Matchers

MatcherDescriptionExample
.toBe(value)Strict equality (===)expect(1 + 1).toBe(2)
.toEqual(value)Deep equality for objects/arraysexpect({a:1}).toEqual({a:1})
.toBeTruthy()Value is truthyexpect("hello").toBeTruthy()
.toBeFalsy()Value is falsyexpect(0).toBeFalsy()
.toBeNull()Value is nullexpect(null).toBeNull()
.toBeGreaterThan(n)Value is greater than nexpect(5).toBeGreaterThan(3)
.toContain(item)Array or string contains itemexpect([1,2,3]).toContain(2)
.toThrow()Function throws an errorexpect(() => fn()).toThrow()
.toHaveLength(n)Array or string has length nexpect([1,2,3]).toHaveLength(3)
.toHaveProperty(key)Object has a specific propertyexpect({name:'A'}).toHaveProperty('name')

Testing Asynchronous Code

Testing with Promises

function fetchUser(id) {
  return new Promise((resolve, reject) => {
    if (id > 0) {
      resolve({ id, name: 'Alice' });
    } else {
      reject(new Error('Invalid ID'));
    }
  });
}

test('fetchUser() resolves with a user object', async function() {
  const user = await fetchUser(1);
  expect(user).toEqual({ id: 1, name: 'Alice' });
  expect(user).toHaveProperty('name');
});

test('fetchUser() rejects for invalid ID', async function() {
  await expect(fetchUser(-1)).rejects.toThrow('Invalid ID');
});

Mocking Functions and Modules

Mocking replaces real functions or modules with fake versions during testing. This is essential for isolating units and avoiding real database calls or API requests in unit tests.

Mocking a Function

// order.js
function sendEmail(to, message) {
  // In real life, this sends an actual email
  console.log("Sending email to:", to);
}

function placeOrder(product, email, mailer) {
  const confirmationCode = Math.floor(Math.random() * 10000);
  mailer(email, `Order confirmed: ${product} (Code: ${confirmationCode})`);
  return { product, email, confirmationCode };
}

module.exports = { placeOrder, sendEmail };
// order.test.js
const { placeOrder } = require('./order');

test('placeOrder() calls the mailer with correct arguments', function() {
  const mockMailer = jest.fn(); // Create a mock function

  const order = placeOrder('Laptop', 'user@example.com', mockMailer);

  expect(mockMailer).toHaveBeenCalledTimes(1);
  expect(mockMailer).toHaveBeenCalledWith(
    'user@example.com',
    expect.stringContaining('Laptop')
  );
  expect(order.product).toBe('Laptop');
});

Setup and Teardown

describe('Database Tests', function() {
  let db;

  // Runs before each test
  beforeEach(function() {
    db = { users: [{ id: 1, name: 'Alice' }] };
  });

  // Runs after each test
  afterEach(function() {
    db = null;
  });

  test('DB has one user initially', function() {
    expect(db.users).toHaveLength(1);
  });

  test('Can add a user', function() {
    db.users.push({ id: 2, name: 'Bob' });
    expect(db.users).toHaveLength(2);
  });
});

Generating a Coverage Report

npm run test:coverage

Jest generates a detailed report showing how much of the codebase is covered by tests — broken down by file, function, line, and branch coverage. A coverage percentage above 70–80% is generally considered good for production projects.

Key Points

  • Testing ensures code behaves as expected and prevents regressions when changes are made.
  • Jest is a zero-configuration testing framework that provides a test runner, assertions, mocks, and coverage in one tool.
  • Tests are organized with describe() and defined with test() or it(). Assertions use expect(value).matcher().
  • Asynchronous tests use async/await with .resolves and .rejects for Promise-based code.
  • Mock functions (jest.fn()) replace real implementations during tests to isolate units.
  • beforeEach and afterEach handle setup and cleanup around individual tests.
  • Code coverage reports show exactly which parts of the code are not being tested.

Leave a Comment

Your email address will not be published. Required fields are marked *