Testing with Jest
Testing is non-negotiable for professional software development. In this chapter, you’ll learn how to write comprehensive tests for your Node.js applications using Jest and Supertest.Why Testing Matters
| Benefit | Description |
|---|---|
| Confidence | Deploy knowing your code works |
| Documentation | Tests describe expected behavior |
| Refactoring | Change code safely with tests as a safety net |
| Bug Prevention | Catch issues before production |
| Design | TDD leads to better architecture |
Testing Pyramid
Copy
/\
/ \ E2E Tests (10%)
/----\ Slow, expensive, fewer
/ \
/--------\ Integration Tests (20%)
/ \ Test component interactions
/------------\
/ \ Unit Tests (70%)
/__________________\ Fast, isolated, many
Setup
Copy
npm install --save-dev jest @types/jest supertest
Copy
// package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"jest": {
"testEnvironment": "node",
"coverageDirectory": "coverage",
"collectCoverageFrom": [
"src/**/*.js",
"!src/**/*.test.js"
],
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}
Writing Your First Test
Copy
// math.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
const multiply = (a, b) => a * b;
const divide = (a, b) => {
if (b === 0) throw new Error('Cannot divide by zero');
return a / b;
};
module.exports = { add, subtract, multiply, divide };
Copy
// math.test.js
const { add, subtract, multiply, divide } = require('./math');
describe('Math functions', () => {
describe('add', () => {
test('should add two positive numbers', () => {
expect(add(2, 3)).toBe(5);
});
test('should add negative numbers', () => {
expect(add(-2, -3)).toBe(-5);
});
test('should add zero', () => {
expect(add(5, 0)).toBe(5);
});
});
describe('divide', () => {
test('should divide two numbers', () => {
expect(divide(10, 2)).toBe(5);
});
test('should throw error when dividing by zero', () => {
expect(() => divide(10, 0)).toThrow('Cannot divide by zero');
});
});
});
Jest Matchers
Copy
describe('Jest Matchers', () => {
// Equality
test('exact equality', () => {
expect(2 + 2).toBe(4);
});
test('object equality', () => {
expect({ name: 'John' }).toEqual({ name: 'John' });
});
// Truthiness
test('truthy/falsy', () => {
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect('value').toBeDefined();
expect(true).toBeTruthy();
expect(false).toBeFalsy();
});
// Numbers
test('number comparisons', () => {
expect(10).toBeGreaterThan(5);
expect(10).toBeGreaterThanOrEqual(10);
expect(5).toBeLessThan(10);
expect(0.1 + 0.2).toBeCloseTo(0.3); // Floating point
});
// Strings
test('string matching', () => {
expect('Hello World').toMatch(/World/);
expect('Hello World').toContain('World');
});
// Arrays
test('array matching', () => {
expect([1, 2, 3]).toContain(2);
expect(['a', 'b', 'c']).toHaveLength(3);
expect([{ id: 1 }, { id: 2 }]).toContainEqual({ id: 1 });
});
// Objects
test('object matching', () => {
expect({ a: 1, b: 2 }).toHaveProperty('a');
expect({ a: 1, b: 2 }).toHaveProperty('a', 1);
expect({ a: 1, b: 2 }).toMatchObject({ a: 1 });
});
// Exceptions
test('exception matching', () => {
expect(() => { throw new Error('fail'); }).toThrow();
expect(() => { throw new Error('fail'); }).toThrow('fail');
expect(() => { throw new Error('fail'); }).toThrow(Error);
});
});
Testing Async Code
Copy
// userService.js
const fetchUser = async (id) => {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error('User not found');
return response.json();
};
const fetchUserCallback = (id, callback) => {
setTimeout(() => {
if (id === 0) callback(new Error('User not found'), null);
else callback(null, { id, name: 'John' });
}, 100);
};
Copy
// userService.test.js
describe('Async Testing', () => {
// Async/await
test('async function success', async () => {
const user = await fetchUser(1);
expect(user).toHaveProperty('name');
});
// Async/await with error
test('async function error', async () => {
await expect(fetchUser(999)).rejects.toThrow('User not found');
});
// Promises
test('promise resolves', () => {
return expect(fetchUser(1)).resolves.toHaveProperty('name');
});
// Callbacks
test('callback success', (done) => {
fetchUserCallback(1, (err, user) => {
expect(err).toBeNull();
expect(user.name).toBe('John');
done();
});
});
});
Mocking
Function Mocks
Copy
// Mock a function
const mockFn = jest.fn();
mockFn('hello');
mockFn('world');
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('hello');
expect(mockFn).toHaveBeenLastCalledWith('world');
// Mock return values
const mockAdd = jest.fn()
.mockReturnValue(10)
.mockReturnValueOnce(5);
expect(mockAdd()).toBe(5); // First call
expect(mockAdd()).toBe(10); // Subsequent calls
// Mock implementation
const mockMultiply = jest.fn((a, b) => a * b);
expect(mockMultiply(3, 4)).toBe(12);
Module Mocks
Copy
// emailService.js
const sendEmail = async (to, subject, body) => {
// Real implementation sends email
};
module.exports = { sendEmail };
Copy
// user.test.js
jest.mock('./emailService');
const { sendEmail } = require('./emailService');
const { registerUser } = require('./userService');
describe('User registration', () => {
beforeEach(() => {
sendEmail.mockClear();
sendEmail.mockResolvedValue({ success: true });
});
test('should send welcome email on registration', async () => {
await registerUser({ email: 'test@example.com', name: 'Test' });
expect(sendEmail).toHaveBeenCalledTimes(1);
expect(sendEmail).toHaveBeenCalledWith(
'test@example.com',
expect.stringContaining('Welcome'),
expect.any(String)
);
});
});
Mocking Modules
Copy
// Automatically mock all module functions
jest.mock('axios');
const axios = require('axios');
test('fetch data', async () => {
axios.get.mockResolvedValue({ data: { id: 1, name: 'John' } });
const user = await getUser(1);
expect(user.name).toBe('John');
expect(axios.get).toHaveBeenCalledWith('/api/users/1');
});
// Manual mock implementation
jest.mock('axios', () => ({
get: jest.fn(() => Promise.resolve({ data: {} })),
post: jest.fn(() => Promise.resolve({ data: {} }))
}));
Testing Express APIs with Supertest
Copy
npm install --save-dev supertest
Copy
// app.js (export app without listening)
const express = require('express');
const app = express();
app.use(express.json());
app.get('/api/users', (req, res) => {
res.json([{ id: 1, name: 'John' }]);
});
app.get('/api/users/:id', (req, res) => {
const id = parseInt(req.params.id);
if (id === 999) return res.status(404).json({ error: 'Not found' });
res.json({ id, name: 'John' });
});
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Name and email required' });
}
res.status(201).json({ id: 1, name, email });
});
module.exports = app;
Copy
// app.test.js
const request = require('supertest');
const app = require('./app');
describe('User API', () => {
describe('GET /api/users', () => {
test('should return all users', async () => {
const response = await request(app)
.get('/api/users')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toBeInstanceOf(Array);
expect(response.body.length).toBeGreaterThan(0);
});
});
describe('GET /api/users/:id', () => {
test('should return user by id', async () => {
const response = await request(app)
.get('/api/users/1')
.expect(200);
expect(response.body).toHaveProperty('id', 1);
expect(response.body).toHaveProperty('name');
});
test('should return 404 for non-existent user', async () => {
await request(app)
.get('/api/users/999')
.expect(404);
});
});
describe('POST /api/users', () => {
test('should create new user', async () => {
const newUser = { name: 'Jane', email: 'jane@example.com' };
const response = await request(app)
.post('/api/users')
.send(newUser)
.expect(201);
expect(response.body).toMatchObject(newUser);
expect(response.body).toHaveProperty('id');
});
test('should return 400 for invalid data', async () => {
await request(app)
.post('/api/users')
.send({ name: 'Jane' }) // Missing email
.expect(400);
});
});
});
Database Testing
Copy
// Setup test database
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
let mongoServer;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
await mongoose.connect(mongoServer.getUri());
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
beforeEach(async () => {
// Clear all collections before each test
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany({});
}
});
describe('User Model', () => {
test('should create user', async () => {
const user = await User.create({
name: 'John',
email: 'john@example.com',
password: 'password123'
});
expect(user._id).toBeDefined();
expect(user.name).toBe('John');
expect(user.password).not.toBe('password123'); // Should be hashed
});
test('should not create user without email', async () => {
await expect(
User.create({ name: 'John', password: 'password123' })
).rejects.toThrow();
});
});
Test Coverage
Copy
# Generate coverage report
npm test -- --coverage
# Output:
# ----------|---------|----------|---------|---------|-------------------
# File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
# ----------|---------|----------|---------|---------|-------------------
# All files | 85.71 | 83.33 | 100 | 85.71 |
# math.js | 100 | 100 | 100 | 100 |
# user.js | 71.43 | 66.67 | 100 | 71.43 | 15-17
# ----------|---------|----------|---------|---------|-------------------
Best Practices
- Name tests clearly: Describe what is being tested and expected outcome
- One assertion per test: Keep tests focused
- Use beforeEach/afterEach: Clean state between tests
- Don’t test implementation: Test behavior, not internals
- Mock external services: Don’t make real API calls in tests
- Aim for 80%+ coverage: But 100% isn’t always necessary
- Run tests in CI/CD: Every push should run tests
Test Organization
Copy
src/
├── services/
│ ├── userService.js
│ └── userService.test.js # Co-located tests
├── models/
│ └── User.js
└── __tests__/ # Or separate test directory
├── integration/
│ └── userApi.test.js
└── unit/
└── userModel.test.js
Summary
- Jest is the most popular testing framework for Node.js
- Use describe/test to organize tests hierarchically
- Matchers like
toBe,toEqual,toThrowvalidate expectations - Mock external dependencies to isolate tests
- Supertest makes API testing straightforward
- Use in-memory databases for database tests
- Aim for high coverage but prioritize critical paths
- Write tests before or alongside code, not as an afterthought