Difficulty: Intermediate
Testing is not optional for professional Node.js applications - it is how you ensure your code works correctly, stays working as you make changes, and can be confidently deployed. Jest is the most popular testing framework for Node.js, providing a complete solution with test runner, assertion library, mocking capabilities, and code coverage reporting out of the box.
Unit tests verify individual functions or modules in isolation. They are fast, focused, and should make up the majority of your test suite. A unit test calls a function with known inputs and asserts that the output matches expectations. For example, testing a password validation function: provide strong and weak passwords, assert it returns true/false correctly. Unit tests should not depend on databases, APIs, or the filesystem - use mocks to replace those dependencies.
Integration tests verify that multiple components work together correctly. For Express APIs, integration tests send HTTP requests to your routes and verify the responses. The `supertest` library makes this easy - it takes your Express app, sends requests without starting a server, and provides a fluent assertion API. You can test entire request/response cycles: send a POST with a body, check the status code, verify the response JSON, and confirm database changes.
Jest's mocking system lets you replace real implementations with controlled fakes. `jest.fn()` creates a mock function that tracks calls and return values. `jest.mock()` replaces an entire module with mocks. This is essential for unit testing - you mock the database client to avoid needing a real database, mock external APIs to avoid network calls, and mock the filesystem to avoid creating files. Mocks ensure your tests are fast, reliable, and independent.
Test organization follows the Arrange-Act-Assert pattern. Arrange: set up the test data and any necessary mocks. Act: call the function or make the request being tested. Assert: verify the result matches expectations. Jest provides `describe()` blocks to group related tests, `it()` or `test()` for individual test cases, and `beforeEach()`/`afterEach()` for setup and cleanup that runs before/after each test.
// utils/validator.js
function validateEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
function validatePassword(password) {
if (password.length < 8) return { valid: false, error: 'Too short' };
if (!/\d/.test(password)) return { valid: false, error: 'Need a number' };
if (!/[A-Z]/.test(password)) return { valid: false, error: 'Need uppercase' };
return { valid: true };
}
// utils/validator.test.js
// describe('validateEmail', () => {
// it('returns true for valid emails', () => {
// expect(validateEmail('user@example.com')).toBe(true);
// expect(validateEmail('a@b.co')).toBe(true);
// });
//
// it('returns false for invalid emails', () => {
// expect(validateEmail('')).toBe(false);
// expect(validateEmail('no-at-sign')).toBe(false);
// expect(validateEmail('@no-local.com')).toBe(false);
// });
// });
//
// describe('validatePassword', () => {
// it('accepts strong passwords', () => {
// expect(validatePassword('Secure1234')).toEqual({ valid: true });
// });
//
// it('rejects short passwords', () => {
// expect(validatePassword('Ab1')).toEqual({ valid: false, error: 'Too short' });
// });
//
// it('requires a number', () => {
// expect(validatePassword('NoNumbers')).toEqual({ valid: false, error: 'Need a number' });
// });
//
// it('requires uppercase letter', () => {
// expect(validatePassword('lowercase1')).toEqual({ valid: false, error: 'Need uppercase' });
// });
// });
console.log('Validate email:', validateEmail('test@example.com'));
console.log('Validate password:', validatePassword('Secure1234'));
console.log('Weak password:', validatePassword('weak'));
Unit tests verify each function's behavior with various inputs. describe() groups related tests. expect().toBe() checks strict equality for primitives. expect().toEqual() does deep equality for objects. Each test case covers a specific scenario - valid input, invalid input, edge cases.
const express = require('express');
// const request = require('supertest');
// Create the app (separate from server startup for testability)
function createApp() {
const app = express();
app.use(express.json());
const users = [];
let nextId = 1;
app.get('/api/users', (req, res) => {
res.json(users);
});
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Name and email required' });
}
const user = { id: nextId++, name, email };
users.push(user);
res.status(201).json(user);
});
return app;
}
// Test file: app.test.js
// const app = createApp();
//
// describe('POST /api/users', () => {
// it('creates a user with valid data', async () => {
// const res = await request(app)
// .post('/api/users')
// .send({ name: 'Alice', email: 'alice@test.com' });
//
// expect(res.status).toBe(201);
// expect(res.body).toHaveProperty('id');
// expect(res.body.name).toBe('Alice');
// expect(res.body.email).toBe('alice@test.com');
// });
//
// it('returns 400 for missing fields', async () => {
// const res = await request(app)
// .post('/api/users')
// .send({ name: 'Alice' });
//
// expect(res.status).toBe(400);
// expect(res.body.error).toBeDefined();
// });
// });
//
// describe('GET /api/users', () => {
// it('returns array of users', async () => {
// const res = await request(app).get('/api/users');
// expect(res.status).toBe(200);
// expect(Array.isArray(res.body)).toBe(true);
// });
// });
const app = createApp();
console.log('App created for testing');
console.log('Pattern: separate createApp() from app.listen() for testability');
The key pattern is separating app creation from server startup. createApp() returns the Express app, which can be imported by both the server (to call listen()) and tests (to pass to supertest). Supertest sends HTTP requests without actually starting the server.
// Service that depends on a database
class UserService {
constructor(db) {
this.db = db;
}
async getUser(id) {
const user = await this.db.findById(id);
if (!user) throw new Error('User not found');
return user;
}
async createUser(data) {
const existing = await this.db.findByEmail(data.email);
if (existing) throw new Error('Email already taken');
return this.db.create(data);
}
}
// Test with mock database
// describe('UserService', () => {
// let service;
// let mockDb;
//
// beforeEach(() => {
// mockDb = {
// findById: jest.fn(),
// findByEmail: jest.fn(),
// create: jest.fn()
// };
// service = new UserService(mockDb);
// });
//
// describe('getUser', () => {
// it('returns user when found', async () => {
// const mockUser = { id: 1, name: 'Alice' };
// mockDb.findById.mockResolvedValue(mockUser);
//
// const user = await service.getUser(1);
// expect(user).toEqual(mockUser);
// expect(mockDb.findById).toHaveBeenCalledWith(1);
// });
//
// it('throws when user not found', async () => {
// mockDb.findById.mockResolvedValue(null);
// await expect(service.getUser(99)).rejects.toThrow('User not found');
// });
// });
//
// describe('createUser', () => {
// it('creates user when email is available', async () => {
// mockDb.findByEmail.mockResolvedValue(null);
// mockDb.create.mockResolvedValue({ id: 1, name: 'Bob', email: 'bob@test.com' });
//
// const user = await service.createUser({ name: 'Bob', email: 'bob@test.com' });
// expect(user.name).toBe('Bob');
// expect(mockDb.create).toHaveBeenCalledTimes(1);
// });
//
// it('throws when email is taken', async () => {
// mockDb.findByEmail.mockResolvedValue({ id: 1, email: 'taken@test.com' });
// await expect(service.createUser({ email: 'taken@test.com' })).rejects.toThrow('Email already taken');
// expect(mockDb.create).not.toHaveBeenCalled();
// });
// });
// });
console.log('UserService defined for testing');
console.log('Mock pattern: inject dependencies, replace with jest.fn()');
The UserService accepts a database object via constructor injection. In tests, we create a mock db with jest.fn() methods. mockResolvedValue sets the return value for async methods. This lets us test the service logic without a real database - fast, reliable, and isolated.
Jest, unit tests, integration tests, supertest, describe/it, expect, mocks, beforeEach