Implementing End-to-End Testing for Node.js Backends Using Jest and Supertest

End-to-end (E2E) testing is essential for ensuring that your Node.js backend APIs function correctly as a complete system. Unlike unit or integration tests, E2E tests validate the entire flow of an application, including external dependencies, database interactions, and third-party APIs.

In this article, we’ll explore how to set up a comprehensive E2E testing suite for Node.js backends using Jest and Supertest. We’ll cover API behavior validation, mocking external services, and strategies for performance testing.

Why End-to-End Testing Matters for Backends

E2E testing provides several advantages:

  • System Validation: Ensures all components work together as expected.

  • Regression Prevention: Detects issues caused by new code or dependency updates.

  • Confidence in Deployment: Reduces the risk of bugs in production.

Tools for E2E Testing

  1. Jest: A popular JavaScript testing framework with built-in mocking, assertions, and coverage tools.

  2. Supertest: A library for testing HTTP endpoints.

Setting Up the Testing Environment

1. Initialize a Node.js Project

Ensure you have a working Node.js backend. For this example, we’ll assume an Express app with a few RESTful endpoints.

Install dependencies:

npm install --save-dev jest supertest

Update your package.json to include a test script:

"scripts": {
  "test": "jest"
}

2. Basic E2E Test with Jest and Supertest

Let’s assume your Express app has an endpoint for fetching a list of users:

Example API (app.js):

const express = require('express');
const app = express();

app.use(express.json());

const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];

app.get('/users', (req, res) => {
    res.status(200).json(users);
});

module.exports = app;

E2E Test (tests/users.test.js):

const request = require('supertest');
const app = require('../app');

describe('GET /users', () => {
    it('should return a list of users', async () => {
        const response = await request(app).get('/users');
        expect(response.status).toBe(200);
        expect(response.body).toEqual([
            { id: 1, name: 'Alice' },
            { id: 2, name: 'Bob' },
        ]);
    });
});

Run the test:

npm test

Expected output:

PASS  tests/users.test.js
✓ should return a list of users (X ms)

Mocking External Services

For APIs relying on third-party services or external APIs, mocking these dependencies is critical to ensure reliable tests without hitting real endpoints.

Example: Mocking a Third-Party API

Original Code (app.js):

const axios = require('axios');

app.get('/weather', async (req, res) => {
    try {
        const response = await axios.get('https://api.weather.com/v3/weather');
        res.status(200).json(response.data);
    } catch (error) {
        res.status(500).json({ error: 'Weather service unavailable' });
    }
});

Mocking with Jest (tests/weather.test.js):

const request = require('supertest');
const app = require('../app');
const axios = require('axios');

jest.mock('axios');

describe('GET /weather', () => {
    it('should return mocked weather data', async () => {
        const mockWeatherData = { temperature: 25, condition: 'Sunny' };
        axios.get.mockResolvedValue({ data: mockWeatherData });

        const response = await request(app).get('/weather');
        expect(response.status).toBe(200);
        expect(response.body).toEqual(mockWeatherData);
    });

    it('should handle weather service errors gracefully', async () => {
        axios.get.mockRejectedValue(new Error('Service unavailable'));

        const response = await request(app).get('/weather');
        expect(response.status).toBe(500);
        expect(response.body).toEqual({ error: 'Weather service unavailable' });
    });
});

Testing with a Database

For endpoints interacting with a database, it’s important to isolate the test data. Use libraries like SQLite, MongoDB Memory Server, or a dedicated test database to avoid polluting production data.

Example: Testing with SQLite

Install SQLite and Sequelize:

npm install sqlite3 sequelize

Test Configuration:

const Sequelize = require('sequelize');
const sequelize = new Sequelize('sqlite::memory:');

const User = sequelize.define('User', {
    name: Sequelize.STRING,
});

beforeAll(async () => {
    await sequelize.sync({ force: true });
    await User.bulkCreate([{ name: 'Alice' }, { name: 'Bob' }]);
});

afterAll(async () => {
    await sequelize.close();
});

it('should fetch users from the database', async () => {
    const users = await User.findAll();
    expect(users.map((u) => u.name)).toEqual(['Alice', 'Bob']);
});

Performance Testing

Performance testing can validate whether your backend performs under load. Jest doesn’t include performance testing features, so consider integrating Artillery or K6.

Example with Artillery:

Install Artillery:

npm install -g artillery

Define a test (load-test.yml):

config:
  target: 'http://localhost:3000'
  phases:
    - duration: 60
      arrivalRate: 10
scenarios:
  - flow:
      - get:
          url: '/users'

Run the load test:

artillery run load-test.yml

Best Practices for E2E Testing

  1. Environment Isolation: Run E2E tests in a controlled environment, such as a Dockerized local setup or dedicated CI environment.

  2. Mock Critical External Dependencies: Avoid relying on third-party services during tests to prevent flakiness.

  3. Database Resetting: Use scripts or tools to reset the database state between tests.

  4. Comprehensive Coverage: Include edge cases, invalid inputs, and performance tests.

Wrapping Up

End-to-end testing is an essential step in ensuring the reliability and performance of Node.js backends. Tools like Jest and Supertest make it easier to validate API behavior, while mocking and performance testing ensure comprehensive coverage.

By implementing a robust E2E testing strategy, you can reduce the risk of regressions, build confidence in deployments, and deliver a seamless experience to your users.

Start small, automate extensively, and continually refine your tests as your application evolves.

0
Subscribe to my newsletter

Read articles from Nicholas Diamond directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nicholas Diamond
Nicholas Diamond