You've written your Express routes, added validation, and set up error handling. But how do you know everything works together? Unit tests verify individual functions, but integration tests verify your APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. endpoints behave correctly when all the pieces connect.
Think of integration testing like test-driving a car. You don't just check that the engine runs (unit testWhat is unit test?An automated check that verifies a single function works correctly in isolation, with all external dependencies replaced by fakes.), you take it on the road and make sure acceleration, braking, and steering work together seamlessly (integration testWhat is integration test?An automated check that verifies multiple parts of your application work together correctly, using real components instead of fakes.).
Setting up Jest and Supertest
Supertest is the gold standard for testing HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. endpoints in Node.js. It lets you make requests to your Express app without starting an actual server.
Install the testing dependencies:
npm install --save-dev jest supertestAdd test scripts to package.jsonWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it.:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}Your first APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. test
Here's a complete test suite for a users API:
// tests/users.test.js
import request from 'supertest';
import app from '../src/app.js';
import { User } from '../src/models/User.js';
describe('Users API', () => {
// Clean database before each test
beforeEach(async () => {
await User.deleteMany();
});
describe('GET /api/users', () => {
it('should return empty array when no users exist', async () => {
const response = await request(app)
.get('/api/users')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body.status).toBe('success');
expect(response.body.data).toEqual([]);
});
it('should return list of users', async () => {
// Arrange: Create test data
await User.create([
{ name: 'John', email: 'john@test.com' },
{ name: 'Jane', email: 'jane@test.com' }
]);
// Act: Make the request
const response = await request(app)
.get('/api/users')
.expect(200);
// Assert: Check the response
expect(response.body.data).toHaveLength(2);
expect(response.body.data[0].name).toBe('John');
expect(response.body.data[1].name).toBe('Jane');
});
it('should support pagination', async () => {
// Create 25 users
const users = Array.from({ length: 25 }, (_, i) => ({
name: `User ${i}`,
email: `user${i}@test.com`
}));
await User.create(users);
const response = await request(app)
.get('/api/users?page=2&limit=10')
.expect(200);
expect(response.body.data).toHaveLength(10);
expect(response.body.meta.pagination.page).toBe(2);
expect(response.body.meta.pagination.total).toBe(25);
});
});
describe('POST /api/users', () => {
it('should create a new user', async () => {
const newUser = {
name: 'Test User',
email: 'test@example.com',
password: 'password123'
};
const response = await request(app)
.post('/api/users')
.send(newUser)
.expect('Content-Type', /json/)
.expect(201);
expect(response.body.status).toBe('success');
expect(response.body.data.name).toBe('Test User');
expect(response.body.data.email).toBe('test@example.com');
expect(response.body.data.password).toBeUndefined(); // Should not return password
expect(response.body.data.id).toBeDefined();
});
it('should return 400 for invalid data', async () => {
const invalidUser = {
name: 'A', // Too short
email: 'not-an-email'
};
const response = await request(app)
.post('/api/users')
.send(invalidUser)
.expect(400);
expect(response.body.status).toBe('error');
expect(response.body.errors).toBeDefined();
expect(response.body.errors.length).toBeGreaterThan(0);
});
it('should return 409 for duplicate email', async () => {
// Create a user first
await User.create({
name: 'Existing',
email: 'duplicate@test.com',
password: 'password123'
});
// Try to create another with same email
const response = await request(app)
.post('/api/users')
.send({
name: 'New User',
email: 'duplicate@test.com',
password: 'password123'
})
.expect(409);
expect(response.body.status).toBe('error');
});
});
describe('GET /api/users/:id', () => {
it('should return user by id', async () => {
const user = await User.create({
name: 'John',
email: 'john@test.com',
password: 'password123'
});
const response = await request(app)
.get(`/api/users/${user.id}`)
.expect(200);
expect(response.body.status).toBe('success');
expect(response.body.data.name).toBe('John');
expect(response.body.data.email).toBe('john@test.com');
});
it('should return 404 for non-existent user', async () => {
const response = await request(app)
.get('/api/users/999999')
.expect(404);
expect(response.body.status).toBe('error');
expect(response.body.message).toContain('not found');
});
it('should return 400 for invalid id format', async () => {
const response = await request(app)
.get('/api/users/invalid-id')
.expect(400);
expect(response.body.status).toBe('error');
});
});
});Notice the pattern: Arrange, Act, Assert. Set up data, make the request, verify the response.
myapp_test alongside myapp_development. Never run tests against your development database, you'll lose your data!Testing authenticated endpoints
Most APIs require authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token.. Here's how to test protected routes:
// tests/auth.test.js
import request from 'supertest';
import app from '../src/app.js';
import { User } from '../src/models/User.js';
import { hashPassword } from '../src/utils/auth.js';
describe('Protected Routes', () => {
let authToken;
let userId;
beforeEach(async () => {
// Create a test user
const user = await User.create({
email: 'test@test.com',
name: 'Test User',
password: await hashPassword('password123')
});
userId = user.id;
// Log in to get token
const loginResponse = await request(app)
.post('/auth/login')
.send({
email: 'test@test.com',
password: 'password123'
});
authToken = loginResponse.body.accessToken;
});
it('should access protected route with valid token', async () => {
const response = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(response.body.data.email).toBe('test@test.com');
});
it('should reject request without token', async () => {
const response = await request(app)
.get('/api/profile')
.expect(401);
expect(response.body.error).toContain('required');
});
it('should reject request with invalid token', async () => {
const response = await request(app)
.get('/api/profile')
.set('Authorization', 'Bearer invalid-token')
.expect(401);
expect(response.body.error).toContain('Invalid');
});
it('should reject expired token', async () => {
// Create an expired token (for testing)
const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
const response = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${expiredToken}`)
.expect(401);
expect(response.body.error).toContain('expired');
});
});The set('Authorization', ...) method adds headers to your requests, simulating how real clients send tokens.
Testing error scenarios
Don't just test the happy path. Errors happen, and your tests should verify you handle them gracefully:
describe('Error Handling', () => {
it('should handle validation errors', async () => {
const response = await request(app)
.post('/api/users')
.send({}) // Empty body
.expect(400);
expect(response.body.status).toBe('error');
expect(response.body.errors).toBeInstanceOf(Array);
});
it('should handle database errors gracefully', async () => {
// Force a database error by disconnecting
await mongoose.connection.close();
const response = await request(app)
.get('/api/users')
.expect(500);
expect(response.body.status).toBe('error');
expect(response.body.message).not.toContain('database'); // Don't leak internals
// Reconnect for other tests
await mongoose.connect(process.env.DATABASE_URL);
});
it('should handle timeout errors', async () => {
// Mock a slow endpoint
jest.spyOn(User, 'find').mockImplementation(() => {
return new Promise((resolve) => setTimeout(resolve, 10000));
});
const response = await request(app)
.get('/api/users')
.timeout(100) // Short timeout
.expect(500);
expect(response.body.status).toBe('error');
});
});Best practices for APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. testing
1. Use a separate test database
// jest.setup.js
beforeAll(async () => {
await mongoose.connect(process.env.TEST_DATABASE_URL);
});
afterAll(async () => {
await mongoose.connection.close();
});
beforeEach(async () => {
// Clean all collections
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany();
}
});2. Test behavior, not implementation
// Good: Test what the API does
expect(response.body.data.name).toBe('John');
// Bad: Test how it's implemented
expect(User.find).toHaveBeenCalledWith({ active: true });3. MockWhat is mock?A fake replacement for a real dependency in tests that records how it was called so you can verify interactions. external services
// Mock email service
jest.mock('../src/services/email.js', () => ({
sendEmail: jest.fn().mockResolvedValue(true)
}));
// Mock external API
jest.mock('axios', () => ({
post: jest.fn().mockResolvedValue({ data: { success: true } })
}));4. Test realistic data
// Good: Realistic test data
const user = {
name: 'Alice Johnson',
email: 'alice.johnson@company.com',
password: 'SecurePass123!'
};
// Bad: Unrealistic data
const user = {
name: 'A',
email: 'a@b.c',
password: 'x'
};5. Test edge cases
it('should handle empty arrays', async () => { ... });
it('should handle very long strings', async () => { ... });
it('should handle special characters', async () => { ... });
it('should handle concurrent requests', async () => { ... });Quick reference: testing patterns
| Pattern | Code | Purpose |
|---|---|---|
| Make request | request(app).get('/url') | Send HTTP request |
| Add headers | .set('Header', 'Value') | Authentication, content-type |
| Send body | .send({ data }) | POST/PUT requests |
| Expect status | .expect(200) | Verify response code |
| Expect content-type | .expect('Content-Type', /json/) | Verify format |
| Clean database | beforeEach(() => deleteMany()) | Test isolation |
Integration tests give you confidence that your APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. works correctly in real-world scenarios. Write them early, run them often, and sleep better knowing your endpoints handle whatever comes their way.