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
Jest: A popular JavaScript testing framework with built-in mocking, assertions, and coverage tools.
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
Environment Isolation: Run E2E tests in a controlled environment, such as a Dockerized local setup or dedicated CI environment.
Mock Critical External Dependencies: Avoid relying on third-party services during tests to prevent flakiness.
Database Resetting: Use scripts or tools to reset the database state between tests.
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.
Subscribe to my newsletter
Read articles from Nicholas Diamond directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by