Node.js & Express

Testing Express APIs

20 min Lesson 27 of 40

Testing Express APIs

Testing your Express API endpoints is essential for ensuring your application works correctly. In this lesson, we'll use Supertest, a library specifically designed for testing HTTP APIs with Express.

Setting Up Supertest

Supertest works seamlessly with Jest and allows you to make HTTP requests to your Express app:

npm install --save-dev supertest

First, let's structure our Express app to be testable. Separate app creation from server startup:

// app.js const express = require('express'); const app = express(); app.use(express.json()); app.get('/api/health', (req, res) => { res.json({ status: 'ok', timestamp: Date.now() }); }); module.exports = app;
// server.js const app = require('./app'); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });
Tip: Separating app creation from server startup allows you to test your app without actually starting a server on a port.

Testing GET Endpoints

Let's create a simple API and test it:

// app.js (continued) const users = [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' } ]; app.get('/api/users', (req, res) => { res.json(users); }); app.get('/api/users/:id', (req, res) => { const user = users.find(u => u.id === parseInt(req.params.id)); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json(user); });
// __tests__/users.test.js 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('Content-Type', /json/) .expect(200); expect(response.body).toHaveLength(2); expect(response.body[0]).toHaveProperty('name', 'Alice'); }); }); describe('GET /api/users/:id', () => { test('should return a specific user', async () => { const response = await request(app) .get('/api/users/1') .expect(200); expect(response.body).toMatchObject({ id: 1, name: 'Alice', email: 'alice@example.com' }); }); test('should return 404 for non-existent user', async () => { const response = await request(app) .get('/api/users/999') .expect(404); expect(response.body).toHaveProperty('error', 'User not found'); }); });

Testing POST Endpoints

Now let's test creating resources:

// app.js (continued) app.post('/api/users', (req, res) => { const { name, email } = req.body; // Validation if (!name || !email) { return res.status(400).json({ error: 'Name and email are required' }); } if (!email.includes('@')) { return res.status(400).json({ error: 'Invalid email format' }); } const newUser = { id: users.length + 1, name, email }; users.push(newUser); res.status(201).json(newUser); });
// __tests__/users.test.js (continued) describe('POST /api/users', () => { test('should create a new user', async () => { const newUser = { name: 'Charlie', email: 'charlie@example.com' }; const response = await request(app) .post('/api/users') .send(newUser) .expect('Content-Type', /json/) .expect(201); expect(response.body).toMatchObject(newUser); expect(response.body).toHaveProperty('id'); }); test('should reject invalid user data', async () => { const invalidUser = { name: 'Charlie' }; // Missing email await request(app) .post('/api/users') .send(invalidUser) .expect(400); }); test('should reject invalid email format', async () => { const invalidUser = { name: 'Charlie', email: 'not-an-email' }; const response = await request(app) .post('/api/users') .send(invalidUser) .expect(400); expect(response.body.error).toContain('Invalid email'); }); });

Testing PUT and DELETE Endpoints

// app.js (continued) app.put('/api/users/:id', (req, res) => { const { name, email } = req.body; const userId = parseInt(req.params.id); const userIndex = users.findIndex(u => u.id === userId); if (userIndex === -1) { return res.status(404).json({ error: 'User not found' }); } if (name) users[userIndex].name = name; if (email) users[userIndex].email = email; res.json(users[userIndex]); }); app.delete('/api/users/:id', (req, res) => { const userId = parseInt(req.params.id); const userIndex = users.findIndex(u => u.id === userId); if (userIndex === -1) { return res.status(404).json({ error: 'User not found' }); } users.splice(userIndex, 1); res.status(204).send(); });
// __tests__/users.test.js (continued) describe('PUT /api/users/:id', () => { test('should update a user', async () => { const updates = { name: 'Alice Updated' }; const response = await request(app) .put('/api/users/1') .send(updates) .expect(200); expect(response.body.name).toBe('Alice Updated'); }); test('should return 404 for non-existent user', async () => { await request(app) .put('/api/users/999') .send({ name: 'Test' }) .expect(404); }); }); describe('DELETE /api/users/:id', () => { test('should delete a user', async () => { await request(app) .delete('/api/users/1') .expect(204); }); test('should return 404 for non-existent user', async () => { await request(app) .delete('/api/users/999') .expect(404); }); });

Testing Middleware

Middleware is a crucial part of Express apps. Let's test authentication middleware:

// middleware/auth.js function authMiddleware(req, res, next) { const token = req.headers.authorization; if (!token) { return res.status(401).json({ error: 'No token provided' }); } if (token !== 'Bearer valid-token') { return res.status(401).json({ error: 'Invalid token' }); } req.user = { id: 1, name: 'Authenticated User' }; next(); } module.exports = authMiddleware;
// app.js (using middleware) const authMiddleware = require('./middleware/auth'); app.get('/api/profile', authMiddleware, (req, res) => { res.json(req.user); });
// __tests__/auth.test.js const request = require('supertest'); const app = require('../app'); describe('Authentication Middleware', () => { test('should reject requests without token', async () => { await request(app) .get('/api/profile') .expect(401); }); test('should reject requests with invalid token', async () => { await request(app) .get('/api/profile') .set('Authorization', 'Bearer invalid-token') .expect(401); }); test('should allow requests with valid token', async () => { const response = await request(app) .get('/api/profile') .set('Authorization', 'Bearer valid-token') .expect(200); expect(response.body).toHaveProperty('name', 'Authenticated User'); }); });
Warning: Never test with real authentication tokens or credentials. Always use mock data in tests.

Database Testing with Test Fixtures

When testing with a database, use test fixtures and proper setup/teardown:

// __tests__/database.test.js const db = require('../database'); const request = require('supertest'); const app = require('../app'); describe('User API with Database', () => { beforeAll(async () => { await db.connect(); }); beforeEach(async () => { // Clear database await db.query('DELETE FROM users'); // Seed test data await db.query( 'INSERT INTO users (name, email) VALUES (?, ?)', ['Test User', 'test@example.com'] ); }); afterAll(async () => { await db.disconnect(); }); test('should fetch users from database', async () => { const response = await request(app) .get('/api/users') .expect(200); expect(response.body).toHaveLength(1); expect(response.body[0].email).toBe('test@example.com'); }); test('should create user in database', async () => { const newUser = { name: 'New User', email: 'new@example.com' }; await request(app) .post('/api/users') .send(newUser) .expect(201); const users = await db.query('SELECT * FROM users'); expect(users).toHaveLength(2); }); });
Note: Use a separate test database or in-memory database (like SQLite :memory:) for testing to avoid affecting production data.

Testing with Environment Variables

// __tests__/setup.js process.env.NODE_ENV = 'test'; process.env.DB_NAME = 'test_database'; process.env.JWT_SECRET = 'test-secret';
// jest.config.js module.exports = { testEnvironment: 'node', setupFilesAfterEnv: ['/__tests__/setup.js'], coveragePathIgnorePatterns: ['/node_modules/'] };

Practice Exercise:

Create a blog API with the following endpoints:

  • GET /api/posts - List all posts
  • GET /api/posts/:id - Get single post
  • POST /api/posts - Create post (requires auth)
  • PUT /api/posts/:id - Update post (requires auth and ownership)
  • DELETE /api/posts/:id - Delete post (requires auth and ownership)

Write comprehensive tests for all endpoints, including authentication, authorization, validation, and error cases. Aim for at least 90% code coverage.

Summary

In this lesson, you learned:

  • Setting up Supertest for API testing
  • Testing GET, POST, PUT, and DELETE endpoints
  • Testing middleware and authentication
  • Database testing with fixtures
  • Setup and teardown for test environments
  • Best practices for API testing