Skip to main content

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

  1. Test behavior, not implementation

    • Test what the API does, not how it does it
    • Focus on inputs and outputs
  2. Use descriptive test names

    • ✅ "should return 404 when user not found"
    • ❌ "test user endpoint"
  3. DRY up tests with setup/teardown

    beforeEach(() => { /* setup */ });
    afterEach(() => { /* cleanup */ });
    beforeAll(() => { /* once before all */ });
    afterAll(() => { /* once after all */ });
  4. Test error cases

    • Test happy paths AND error scenarios
    • Verify error messages are helpful
  5. Mock external dependencies

    • Database, APIs, file system
    • Isolate the code being tested
  6. Keep tests fast

    • Use in-memory databases for tests
    • Avoid sleep/delays when possible
    • Run tests in parallel
  7. 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