Testing Express Applications
Why Test?
Testing ensures your Express application:
- Works as expected under various conditions
- Doesn't break existing functionality when changes are made
- Handles edge cases and errors properly
- Is maintainable and reliable for long-term use
Testing Types
- Unit Tests: Test individual functions/methods in isolation
- Integration Tests: Test how components work together (database, middleware, etc.)
- API Tests: Test HTTP endpoints and request/response cycles
- End-to-End Tests: Test complete user workflows
Setup
Install Testing Tools
Install Jest and Supertest
npm install --save-dev jest supertest
- Jest: Test framework and assertion library
- Supertest: HTTP assertion library for testing Express apps
Configure Jest
jest.config.js
module.exports = {
testEnvironment: 'node',
coveragePathIgnorePatterns: ['/node_modules/'],
testMatch: ['**/__tests__/**/*.js', '**/?(*.)+(spec|test).js'],
collectCoverageFrom: [
'src/**/*.js',
'!src/index.js'
]
};
Update package.json
package.json scripts
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
Basic API Test
Basic Express API test
const request = require('supertest');
const app = require('../app');
describe('GET /api/users', () => {
test('should return all users', async () => {
const response = await request(app)
.get('/api/users')
.expect(200);
expect(response.body).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
name: expect.any(String)
})
])
);
});
test('should return 404 if user not found', async () => {
const response = await request(app)
.get('/api/users/999')
.expect(404);
expect(response.body).toHaveProperty('error');
});
});
Unit Testing Express Middleware
Testing custom middleware
const authMiddleware = require('../middleware/auth');
describe('Auth Middleware', () => {
let req, res, next;
beforeEach(() => {
req = {
headers: {}
};
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};
next = jest.fn();
});
test('should call next if token is valid', () => {
req.headers.authorization = 'Bearer valid-token';
// Mock the token verification
jest.mock('../utils/verifyToken', () => ({
verify: jest.fn().mockReturnValue({ id: 1 })
}));
authMiddleware(req, res, next);
expect(next).toHaveBeenCalled();
expect(req.user).toEqual({ id: 1 });
});
test('should return 401 if no token provided', () => {
authMiddleware(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(next).not.toHaveBeenCalled();
});
});
Integration Testing Database
Testing with database interactions
const request = require('supertest');
const app = require('../app');
const User = require('../models/User');
describe('User API Integration Tests', () => {
beforeEach(async () => {
// Clear database before each test
await User.deleteMany({});
});
test('should create a new user', async () => {
const response = await request(app)
.post('/api/users')
.send({
name: 'John Doe',
email: 'john@example.com'
})
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe('John Doe');
// Verify in database
const user = await User.findOne({ email: 'john@example.com' });
expect(user).not.toBeNull();
});
test('should not create user with invalid email', async () => {
const response = await request(app)
.post('/api/users')
.send({
name: 'John Doe',
email: 'invalid-email'
})
.expect(400);
expect(response.body).toHaveProperty('error');
});
test('should update a user', async () => {
const user = await User.create({
name: 'John',
email: 'john@example.com'
});
const response = await request(app)
.put(`/api/users/${user._id}`)
.send({ name: 'Jane' })
.expect(200);
expect(response.body.name).toBe('Jane');
});
test('should delete a user', async () => {
const user = await User.create({
name: 'John',
email: 'john@example.com'
});
await request(app)
.delete(`/api/users/${user._id}`)
.expect(204);
const deleted = await User.findById(user._id);
expect(deleted).toBeNull();
});
});
Mocking External Services
Mocking HTTP requests to external APIs
const request = require('supertest');
const app = require('../app');
const axios = require('axios');
jest.mock('axios');
describe('Weather API Integration', () => {
test('should fetch weather data', async () => {
// Mock the external API response
axios.get.mockResolvedValue({
data: {
temperature: 25,
conditions: 'Sunny'
}
});
const response = await request(app)
.get('/api/weather?city=New York')
.expect(200);
expect(response.body.temperature).toBe(25);
expect(axios.get).toHaveBeenCalledWith(
expect.stringContaining('weather'),
expect.any(Object)
);
});
test('should handle API errors gracefully', async () => {
axios.get.mockRejectedValue(new Error('API Error'));
const response = await request(app)
.get('/api/weather?city=New York')
.expect(500);
expect(response.body).toHaveProperty('error');
});
});
Testing Error Handlers
Testing error handling middleware
const request = require('supertest');
const express = require('express');
describe('Error Handling', () => {
let app;
beforeEach(() => {
app = express();
app.get('/error', (req, res, next) => {
next(new Error('Test error'));
});
// Error handler
app.use((err, req, res, next) => {
res.status(err.status || 500).json({
success: false,
error: err.message
});
});
});
test('should handle thrown errors', async () => {
const response = await request(app)
.get('/error')
.expect(500);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Test error');
});
test('should return 404 for undefined routes', async () => {
const response = await request(app)
.get('/undefined-route')
.expect(404);
});
});
Testing Authentication
Testing protected endpoints
const request = require('supertest');
const app = require('../app');
const jwt = require('jsonwebtoken');
describe('Authentication Tests', () => {
let token;
beforeEach(() => {
token = jwt.sign({ id: 1 }, process.env.JWT_SECRET);
});
test('should access protected route with valid token', async () => {
const response = await request(app)
.get('/api/protected')
.set('Authorization', `Bearer ${token}`)
.expect(200);
});
test('should reject request without token', async () => {
const response = await request(app)
.get('/api/protected')
.expect(401);
expect(response.body).toHaveProperty('error');
});
test('should reject invalid token', async () => {
const response = await request(app)
.get('/api/protected')
.set('Authorization', 'Bearer invalid-token')
.expect(401);
});
test('should reject expired token', async () => {
const expiredToken = jwt.sign(
{ id: 1 },
process.env.JWT_SECRET,
{ expiresIn: '0s' }
);
const response = await request(app)
.get('/api/protected')
.set('Authorization', `Bearer ${expiredToken}`)
.expect(401);
});
});
Testing Form Data and File Uploads
Testing file uploads with Supertest
const request = require('supertest');
const app = require('../app');
const fs = require('fs');
const path = require('path');
describe('File Upload', () => {
test('should upload a file', async () => {
const filePath = path.join(__dirname, 'test-file.txt');
// Create test file
fs.writeFileSync(filePath, 'test content');
const response = await request(app)
.post('/api/upload')
.attach('file', filePath)
.expect(200);
expect(response.body).toHaveProperty('filename');
// Cleanup
fs.unlinkSync(filePath);
});
test('should reject missing file', async () => {
const response = await request(app)
.post('/api/upload')
.expect(400);
expect(response.body).toHaveProperty('error');
});
test('should reject invalid file type', async () => {
const filePath = path.join(__dirname, 'test-file.exe');
fs.writeFileSync(filePath, 'test');
const response = await request(app)
.post('/api/upload')
.attach('file', filePath)
.expect(400);
fs.unlinkSync(filePath);
});
});
Test Helpers
Reusable test helpers
// helpers/test-helpers.js
const jwt = require('jsonwebtoken');
const createTestToken = (userId = 1) => {
return jwt.sign({ id: userId }, process.env.JWT_SECRET);
};
const getAuthHeader = (token) => {
return { Authorization: `Bearer ${token}` };
};
const createTestUser = async (User, data = {}) => {
return User.create({
name: 'Test User',
email: 'test@example.com',
...data
});
};
module.exports = {
createTestToken,
getAuthHeader,
createTestUser
};
Usage:
Using test helpers
const { createTestToken, getAuthHeader } = require('../helpers/test-helpers');
test('should access protected route', async () => {
const token = createTestToken(1);
const headers = getAuthHeader(token);
await request(app)
.get('/api/protected')
.set(headers)
.expect(200);
});
Test Organization
project/
├── src/
│ ├── models/
│ ├── routes/
│ ├── middleware/
│ └── app.js
├── tests/
│ ├── unit/
│ │ ├── middleware.test.js
│ │ └── utils.test.js
│ ├── integration/
│ │ ├── users.test.js
│ │ ├── posts.test.js
│ │ └── auth.test.js
│ ├── fixtures/
│ │ └── users.json
│ └── setup.js
└── jest.config.js
Best Practices
-
Test behavior, not implementation
- Test what the API does, not how it does it
- Focus on inputs and outputs
-
Use descriptive test names
- ✅ "should return 404 when user not found"
- ❌ "test user endpoint"
-
DRY up tests with setup/teardown
beforeEach(() => { /* setup */ });afterEach(() => { /* cleanup */ });beforeAll(() => { /* once before all */ });afterAll(() => { /* once after all */ }); -
Test error cases
- Test happy paths AND error scenarios
- Verify error messages are helpful
-
Mock external dependencies
- Database, APIs, file system
- Isolate the code being tested
-
Keep tests fast
- Use in-memory databases for tests
- Avoid sleep/delays when possible
- Run tests in parallel
-
Aim for good coverage
- Target 80%+ coverage
- Focus on critical paths
- Coverage ≠ Quality
Running Tests
Run tests
npm test
Watch mode
npm run test:watch
Coverage report
npm run test:coverage
Key Takeaways
- Use Jest and Supertest for testing Express applications
- Test API endpoints, middleware, error handlers, and authentication
- Mock external services and databases
- Use beforeEach/afterEach for setup and cleanup
- Test error cases and edge cases, not just happy paths
- Keep tests isolated and independent
- Aim for meaningful test coverage