Robust testing is essential for maintaining reliable Node.js applications. This guide covers unit, integration, and end-to-end testing with modern JavaScript testing tools.
1. Testing Pyramid Overview
Unit Tests
- Test individual functions/components
- Fast execution
- High isolation (mocks/stubs)
- ~70% of test coverage
Integration Tests
- Test component interactions
- Include some external services
- ~20% of test coverage
E2E Tests
- Test complete workflows
- Slowest but most realistic
- ~10% of test coverage
2. Testing Setup with Jest
Basic Configuration
// package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"jest": {
"testEnvironment": "node",
"coveragePathIgnorePatterns": [
"/node_modules/",
"/tests/"
],
"setupFilesAfterEnv": ["./tests/setup.js"]
}
}
// Install Jest
npm install --save-dev jest @types/jest
Basic Test Example
// utils/calculator.js
function add(a, b) {
return a + b;
}
module.exports = { add };
// tests/calculator.test.js
const { add } = require('../utils/calculator');
describe('Calculator', () => {
describe('add()', () => {
it('should add two numbers correctly', () => {
expect(add(2, 3)).toBe(5);
});
it('should handle negative numbers', () => {
expect(add(-1, -1)).toBe(-2);
});
});
});
3. Testing Asynchronous Code
Callbacks
test('fetchData with callback', done => {
fetchData((err, data) => {
expect(err).toBeNull();
expect(data).toEqual({ id: 1 });
done(); // Test completes
});
});
Promises
test('fetchData with promises', () => {
return fetchData().then(data => {
expect(data).toEqual({ id: 1 });
});
});
// OR with async/await
test('fetchData async/await', async () => {
const data = await fetchData();
expect(data).toEqual({ id: 1 });
});
4. Mocking Dependencies
Manual Mocks
// __mocks__/fs.js
module.exports = {
readFile: jest.fn((path, cb) =>
cb(null, 'mock file content'))
};
// In test file
jest.mock('fs');
test('reads file content', () => {
const fs = require('fs');
fs.readFile.mockImplementation((path, cb) =>
cb(null, 'custom content'));
readFile('test.txt', (err, data) => {
expect(data).toBe('custom content');
});
});
Jest Automatic Mocks
jest.mock('../services/userService');
const userService = require('../services/userService');
userService.getUser.mockResolvedValue({ id: 1 });
test('gets user', async () => {
const user = await getUser(1);
expect(userService.getUser).toHaveBeenCalledWith(1);
expect(user).toEqual({ id: 1 });
});
5. Integration Testing
API Testing with Supertest
const request = require('supertest');
const app = require('../app');
describe('GET /api/users', () => {
it('should return user list', async () => {
const res = await request(app)
.get('/api/users')
.expect(200);
expect(res.body).toBeInstanceOf(Array);
expect(res.body[0]).toHaveProperty('id');
});
it('should 404 for invalid route', async () => {
await request(app)
.get('/api/nonexistent')
.expect(404);
});
});
Database Testing
const mongoose = require('mongoose');
const User = require('../models/User');
beforeAll(async () => {
await mongoose.connect(process.env.TEST_DB_URI);
});
afterEach(async () => {
await User.deleteMany();
});
afterAll(async () => {
await mongoose.disconnect();
});
test('creates user', async () => {
const user = await User.create({
name: 'Test',
email: 'test@test.com'
});
expect(user._id).toBeDefined();
expect(user.name).toBe('Test');
});
6. End-to-End Testing
Puppeteer Example
const puppeteer = require('puppeteer');
describe('Frontend E2E', () => {
let browser, page;
beforeAll(async () => {
browser = await puppeteer.launch();
page = await browser.newPage();
});
afterAll(async () => {
await browser.close();
});
test('homepage loads', async () => {
await page.goto('http://localhost:3000');
await page.waitForSelector('#app');
const title = await page.title();
expect(title).toBe('My App');
const heading = await page.$eval('h1', el => el.textContent);
expect(heading).toContain('Welcome');
});
});
7. Advanced Testing Techniques
Snapshot Testing
test('renders component correctly', () => {
const component = renderComponent({ name: 'Test' });
expect(component).toMatchSnapshot();
});
Property-Based Testing
const fc = require('fast-check');
test('addition is commutative', () => {
fc.assert(
fc.property(fc.integer(), fc.integer(), (a, b) => {
return add(a, b) === add(b, a);
})
);
});
Next: Deploying Node.js Applications →
Testing Best Practices
- Test behavior, not implementation
- Keep tests isolated and independent
- Use descriptive test names
- Run tests in CI/CD pipeline
- Aim for meaningful coverage (not just 100%)
- Tag slow tests as such (
jest --runInBand
)