Express routes don’t actually execute your business logic when you’re testing them; they just return a specific HTTP response based on the request.
Let’s see it in action. Imagine you have a simple Express app with a single route:
// app.js
const express = require('express');
const app = express();
const port = 3000;
app.get('/users/:id', (req, res) => {
const userId = req.params.id;
if (userId === '123') {
res.json({ id: userId, name: 'Alice' });
} else {
res.status(404).send('User not found');
}
});
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
module.exports = app; // Export the app for testing
Now, let’s test this route using Jest and Supertest. Supertest is a library that allows you to easily test HTTP servers.
First, install the necessary packages:
npm install --save-dev jest supertest
Next, create a test file (e.g., app.test.js):
// app.test.js
const request = require('supertest');
const app = require('./app'); // Import your Express app
describe('User Routes', () => {
test('should return user data for a valid ID', async () => {
const response = await request(app)
.get('/users/123');
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ id: '123', name: 'Alice' });
});
test('should return 404 for an invalid ID', async () => {
const response = await request(app)
.get('/users/456');
expect(response.statusCode).toBe(404);
expect(response.text).toBe('User not found');
});
});
To run the tests, add a script to your package.json:
// package.json
{
"scripts": {
"test": "jest"
}
}
And then run:
npm test
The tests will pass, demonstrating that Supertest can make HTTP requests to your Express app without needing to start the server in the traditional sense. It essentially simulates requests and captures the responses.
The core problem Supertest solves is decoupling your route handlers from a running HTTP server. Normally, an Express app listens on a port, and requests come in over the network. For testing, this is cumbersome. You’d have to start and stop the server for each test, which is slow and prone to race conditions. Supertest bypasses the network entirely. When you call request(app), Supertest takes your Express app instance and directly calls its internal request handling mechanism. It simulates the request object (req) and response object (res) that Express expects, and then captures the output of your route handler. This allows for fast, isolated, and reliable testing of your API endpoints.
The request(app) part is key. You’re not passing a URL or a port. You’re passing the actual Express application object. Supertest then acts as a mock client, sending requests directly into your app’s middleware stack. It intercepts the res.send(), res.json(), res.status(), etc., calls that your route handlers make. The response object you get back from await request(app).get(...) contains properties like statusCode, body (if res.json() was used), and text (if res.send() was used). This makes it feel like you’re interacting with a real HTTP server, but without any of the overhead.
When you use request(app).get('/users/123'), Supertest constructs a mock req object with req.params.id set to '123' and calls your app’s internal handler. Your handler then executes its logic, finds the user, and calls res.json({ id: userId, name: 'Alice' }). Supertest captures this res object, and the .body property on the response variable in your test will be populated with the JSON object. For the 404 case, res.status(404) sets the status code, and res.send('User not found') populates the text body. Supertest makes these captured values available for your expect assertions.
You might be wondering about middleware. Supertest executes all middleware that comes before your route handler. This is crucial because it means your tests will accurately reflect how your application behaves with its middleware stack. If you have authentication middleware, body parsers, or logging middleware, they will all run. This is why it’s important to export your Express app instance from its main file so you can import it into your test files.
The most surprising thing about testing Express routes with Supertest is that you are not actually making network requests. You are directly invoking your Express application’s internal request/response cycle. Supertest constructs mock req and res objects and passes them to your application’s dispatch mechanism, effectively bypassing the network stack entirely. This is why it’s so fast and reliable.
The next step is often testing more complex scenarios, like handling POST requests with JSON bodies or testing middleware that modifies the request or response objects.