Skip to main content

Express Jest Integration

Introduction

Testing is a crucial part of the development process, particularly for web applications where reliability and performance are essential. In this guide, we'll explore how to integrate Jest, a popular JavaScript testing framework, with Express applications to create comprehensive test suites for your API endpoints.

Jest provides a rich set of tools for writing tests, including assertion libraries, mocking capabilities, and code coverage reports. When combined with Express, it enables developers to thoroughly test API endpoints, middleware functions, and request handlers to ensure your application works correctly under various scenarios.

Setting Up Jest with Express

Installation

Let's start by adding Jest and some helpful testing utilities to your Express project:

bash
npm install --save-dev jest supertest
  • Jest: The testing framework that will run our tests
  • Supertest: A library that helps with HTTP assertions, making it easy to test API endpoints

Basic Configuration

Create a jest.config.js file in your project root:

javascript
module.exports = {
testEnvironment: 'node',
verbose: true,
collectCoverage: true,
coverageDirectory: 'coverage',
testMatch: ['**/__tests__/**/*.js', '**/?(*.)+(spec|test).js'],
};

Update your package.json to include test scripts:

json
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}

Your First Express Jest Test

Let's create a simple Express app and test it with Jest. First, here's a basic Express application:

javascript
// app.js
const express = require('express');
const app = express();

app.use(express.json());

app.get('/api/hello', (req, res) => {
res.status(200).json({ message: 'Hello, World!' });
});

app.post('/api/users', (req, res) => {
const { name } = req.body;

if (!name) {
return res.status(400).json({ error: 'Name is required' });
}

res.status(201).json({ id: Date.now(), name });
});

module.exports = app; // Export the app for testing

Now let's write a test file for this Express application:

javascript
// app.test.js
const request = require('supertest');
const app = require('./app');

describe('API Endpoints', () => {
test('GET /api/hello should return a greeting message', async () => {
const response = await request(app).get('/api/hello');

expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ message: 'Hello, World!' });
});

test('POST /api/users should create a new user', async () => {
const userData = { name: 'John Doe' };

const response = await request(app)
.post('/api/users')
.send(userData)
.set('Accept', 'application/json');

expect(response.statusCode).toBe(201);
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe('John Doe');
});

test('POST /api/users should return 400 if name is missing', async () => {
const response = await request(app)
.post('/api/users')
.send({})
.set('Accept', 'application/json');

expect(response.statusCode).toBe(400);
expect(response.body).toEqual({ error: 'Name is required' });
});
});

Testing Express Middleware

Middleware functions are an essential part of Express applications. Let's see how to test them:

First, let's create a simple authentication middleware:

javascript
// middleware/auth.js
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;

if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Unauthorized' });
}

const token = authHeader.split(' ')[1];
if (token !== 'valid-token') {
return res.status(403).json({ error: 'Forbidden' });
}

req.user = { id: 123, name: 'Authenticated User' };
next();
}

module.exports = authMiddleware;

Now let's test this middleware:

javascript
// middleware/auth.test.js
const authMiddleware = require('./auth');

describe('Authentication Middleware', () => {
let req;
let res;
let next;

beforeEach(() => {
req = { headers: {} };
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};
next = jest.fn();
});

test('should return 401 if no authorization header is present', () => {
authMiddleware(req, res, next);

expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({ error: 'Unauthorized' });
expect(next).not.toHaveBeenCalled();
});

test('should return 401 if authorization header is invalid', () => {
req.headers.authorization = 'InvalidFormat';

authMiddleware(req, res, next);

expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({ error: 'Unauthorized' });
expect(next).not.toHaveBeenCalled();
});

test('should return 403 if token is invalid', () => {
req.headers.authorization = 'Bearer invalid-token';

authMiddleware(req, res, next);

expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({ error: 'Forbidden' });
expect(next).not.toHaveBeenCalled();
});

test('should call next() and set user if token is valid', () => {
req.headers.authorization = 'Bearer valid-token';

authMiddleware(req, res, next);

expect(next).toHaveBeenCalled();
expect(req.user).toEqual({ id: 123, name: 'Authenticated User' });
});
});

Testing Routes with Database Interactions

In real-world applications, your routes often interact with databases. Let's see how to test them using mocks:

First, let's create a user controller that interacts with a database:

javascript
// controllers/userController.js
const User = require('../models/User');

async function getUsers(req, res) {
try {
const users = await User.findAll();
res.status(200).json(users);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch users' });
}
}

async function getUserById(req, res) {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.status(200).json(user);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch user' });
}
}

module.exports = {
getUsers,
getUserById
};

Now, let's test these controller functions by mocking the database model:

javascript
// controllers/userController.test.js
const userController = require('./userController');
const User = require('../models/User');

// Mock the User model
jest.mock('../models/User');

describe('User Controller', () => {
let req;
let res;

beforeEach(() => {
req = {};
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};
});

describe('getUsers', () => {
test('should return all users', async () => {
const mockUsers = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];

User.findAll.mockResolvedValue(mockUsers);

await userController.getUsers(req, res);

expect(User.findAll).toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(mockUsers);
});

test('should handle errors', async () => {
User.findAll.mockRejectedValue(new Error('Database error'));

await userController.getUsers(req, res);

expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({ error: 'Failed to fetch users' });
});
});

describe('getUserById', () => {
test('should return user if found', async () => {
const mockUser = { id: '123', name: 'John' };
req.params = { id: '123' };

User.findById.mockResolvedValue(mockUser);

await userController.getUserById(req, res);

expect(User.findById).toHaveBeenCalledWith('123');
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(mockUser);
});

test('should return 404 if user not found', async () => {
req.params = { id: '999' };

User.findById.mockResolvedValue(null);

await userController.getUserById(req, res);

expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith({ error: 'User not found' });
});
});
});

Integration Testing with Express and Jest

So far we've been testing components in isolation. Now let's put it all together with an integration test:

javascript
// integration.test.js
const request = require('supertest');
const mongoose = require('mongoose');
const app = require('./app');
const User = require('./models/User');

// Use a test database
beforeAll(async () => {
const dbUrl = 'mongodb://localhost:27017/test-db';
await mongoose.connect(dbUrl);
});

afterAll(async () => {
await mongoose.connection.close();
});

beforeEach(async () => {
// Clear the database before each test
await User.deleteMany({});
});

describe('User API Integration', () => {
test('should create and retrieve users', async () => {
// Create a user
const createResponse = await request(app)
.post('/api/users')
.send({ name: 'Test User', email: '[email protected]' })
.set('Accept', 'application/json');

expect(createResponse.status).toBe(201);
expect(createResponse.body).toHaveProperty('id');

const userId = createResponse.body.id;

// Get the created user
const getResponse = await request(app)
.get(`/api/users/${userId}`);

expect(getResponse.status).toBe(200);
expect(getResponse.body.name).toBe('Test User');
expect(getResponse.body.email).toBe('[email protected]');

// Get all users
const getAllResponse = await request(app)
.get('/api/users');

expect(getAllResponse.status).toBe(200);
expect(getAllResponse.body).toBeInstanceOf(Array);
expect(getAllResponse.body).toHaveLength(1);
});

test('should handle validation errors', async () => {
// Try to create a user without required fields
const response = await request(app)
.post('/api/users')
.send({})
.set('Accept', 'application/json');

expect(response.status).toBe(400);
expect(response.body).toHaveProperty('error');
});
});

Best Practices for Testing Express with Jest

  1. Separate app and server: Export your Express app separately from the server setup so it can be imported for testing without starting the server.

  2. Use environment variables: Configure your app to use different databases for testing and production.

  3. Mock external services: When testing API endpoints that call external services, use Jest's mocking capabilities.

  4. Test error handling: Make sure to test both the happy path and error cases.

  5. Test HTTP status codes and response bodies: Verify that your API returns the correct status codes and response formats.

  6. Clean up after tests: If your tests modify a database, clean up the changes after the tests run.

  7. Group related tests: Use describe blocks to organize tests by endpoint or functionality.

  8. Keep tests independent: Each test should be able to run independently of other tests.

Troubleshooting Common Issues

1. Tests are affecting each other

If your tests are affecting each other, make sure you're cleaning up resources (like database records) between tests.

javascript
beforeEach(async () => {
// Reset the database or mocks before each test
await YourModel.deleteMany({});
});

2. Asynchronous tests aren't completing

Make sure you're properly handling asynchronous code in your tests:

javascript
// ❌ Wrong way - test might complete before the async operation
test('async operation', () => {
someAsyncFunction().then(result => {
expect(result).toBe(true);
});
});

// ✅ Correct way - using async/await
test('async operation', async () => {
const result = await someAsyncFunction();
expect(result).toBe(true);
});

// ✅ Alternative correct way - using done callback
test('async operation', (done) => {
someAsyncFunction().then(result => {
expect(result).toBe(true);
done();
});
});

3. Mock functions aren't working

Make sure your mocks are set up before they're used:

javascript
// Make sure this comes before any imports that use the module
jest.mock('./path/to/module');

// Or mock individual functions
const originalModule = jest.requireActual('./module');
jest.mock('./module', () => ({
...originalModule,
specificFunction: jest.fn()
}));

Summary

In this guide, we've covered the essential aspects of integrating Jest with Express applications:

  1. Setting up Jest and Supertest with an Express application
  2. Writing basic tests for API endpoints
  3. Testing Express middleware functions
  4. Testing routes that interact with databases using mocks
  5. Creating integration tests to verify the entire system works correctly
  6. Best practices and common troubleshooting tips

By following these practices, you can create a robust test suite that helps maintain the quality and reliability of your Express application. Testing may require extra effort initially, but it pays off by preventing bugs and making your codebase more maintainable in the long run.

Additional Resources

Exercises

  1. Create a complete test suite for a RESTful API with CRUD operations
  2. Write tests for an authentication system with JWT
  3. Implement integration tests for an Express app that uses a database
  4. Add tests for file upload functionality in Express
  5. Practice TDD by writing tests first, then implementing the Express routes to make them pass


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)