A testing guide for Express with request and response mocking/stubbing using Jest or sinon
To test an Express handler, it’s useful to know how to successfully mock/stub the request
and response
objects. The following examples will be written both using Jest and sinon (running in AVA).
The rationale for this is the following. Jest is a very popular “all-in-one” testing framework. Sinon is one of the most popular “Standalone test spies, stubs and mocks for JavaScript” which “works with any unit testing framework”.
The approach detailed in this post will be about how to test handlers independently of the Express app instance by calling them directly with mocked request (req
) and response (res
) objects. This is only 1 approach to testing Express handlers and middleware. The alternative is to fire up the Express server (ideally in-memory using SuperTest). I go into more detail on how to achieve that in “Testing an Express app with SuperTest, moxios and Jest”.
One of the big conceptual leaps to testing Express applications with mocked request/response is understanding how to mock a chained API eg. res.status(200).json({ foo: 'bar' })
.
This is achieved by returning the res
instance from each of its methods:
const mockResponse = () => {
const res = {};
// replace the following () => res
// with your function stub/mock of choice
// making sure they still return `res`
res.status = () => res;
res.json = () => res;
return res;
};
See the repository with examples and the working application at github.com/HugoDF/mock-express-request-response.
Table of contents:
Table of Contents
Stubs and mocks: Jest.fn vs sinon
jest.fn
and sinon.stub
have the same role. They both return a mock/stub for a function. That just means a function that recalls information about its calls, eg. how many times and what arguments it was called with.
The Jest mock is tightly integrated with the rest of the framework. That means we can have assertions that look like the following:
test('jest.fn recalls what it has been called with', () => {
const mock = jest.fn();
mock('a', 'b', 'c');
expect(mock).toHaveBeenCalledTimes(1);
expect(mock).toHaveBeenCalledWith('a', 'b', 'c');
});
Sinon is “just” a spies/stubs/mocks library, that means we need a separate test runner, the following example is equivalent to the previous Jest one but written using AVA:
const test = require('ava');
const sinon = require('sinon');
test('sinon.stub recalls what it has been called with', t => {
const mock = sinon.stub();
mock('a', 'b', 'c');
t.true(mock.called);
t.true(mock.calledWith('a', 'b', 'c'));
});
Mocking/stubbing a chained API: Express response
The Express user-land API is based around middleware. A middleware that takes a request (usually called req
), a response (usually called res
) and a next (call next middleware) as parameters.
A “route handler” is a middleware that tends not to call next
, it usually results in a response being sent.
An example of some route handlers are the following (in express-handlers.js).
In this example req.session
is generated by client-sessions
, a middleware by Mozilla that sets an encrypted cookie that gets set on the client (using a Set-Cookie
). That’s beyond the scope of this post. For all intents and purposes, we could be accessing/writing to any other set of request/response properties.
async function logout(req, res) {
req.session.data = null;
return res.status(200).json();
}
async function checkAuth(req, res) {
if (!req.session.data) {
return res.status(401).json();
}
const { username } = req.session.data;
return res.status(200).json({ username });
}
module.exports = {
logout,
checkAuth
};
They are consumed by being “mounted” on an Express application (app
) instance (in app.js):
const express = require('express');
const app = express();
const { logout, checkAuth } = require('./express-handlers.js');
app.get('/session', checkAuth);
app.delete('/session', logout);
For the above code to work in an integrated manner, we need to also app.use
the client-sessions
package like so. Note that the cookieName
is important since it’s the property under which the session gets set on the req
object.
We also add the express.json
middleware (Express 4.16+), which works like body-parser’s .json()
option ie. it parses JSON bodies and stores the output in to req.body
.
const express = require('express');
const app = express();
const session = require('client-sessions');
app.use(express.json());
app.use(session({
secret: process.env.SESSION_SECRET || 'my-super-secret',
cookieName: 'session',
duration: 60 * 60 * 1000 // 1 hour
}));
const { logout, checkAuth } = require('./express-handlers.js');
app.get('/session', checkAuth);
app.delete('/session', logout);
Mocking/stubbing req (a simple Express request) with Jest or sinon
A mockRequest function needs to return a request-compatible object, which is a plain JavaScript object, it could look like the following, depending on what properties of req
the code under test is using. Our code only accesses req.session.data
, it means it’s expecting req
to have a session
property which is an object so that it can attempt to access the req.session.data
property.
const mockRequest = (sessionData) => {
return {
session: { data: sessionData },
};
};
Since the above is just dealing with data, there’s no difference between mocking it in Jest or using sinon and the test runner of your choice (Mocha, AVA, tape, Jasmine…).
Mocking/stubbing res (a simple Express response) with Jest
A mockResponse function would look like the following, our code under test only calls status
and json
functions. The issue we run into is that the calls are chained. This means that status
, json
and other res
(Express response) methods return the res
object itself.
That means that ideally our mock would behave in the same way:
const mockResponse = () => {
const res = {};
res.status = jest.fn().mockReturnValue(res);
res.json = jest.fn().mockReturnValue(res);
return res;
};
We’re leveraging jest.fn
’s mockReturnValue
method to set the return value of both status
and json
to the mock response instance (res
) they’re set on.
Mocking/stubbing res (a simple Express response) with sinon
The sinon equivalent to the above (with a similar explanation) follows. With sinon, we have to explicitly require
it since it’s a standalone library (ie. not injected by test frameworks).
Sinon stubs have a returns
method which behaves like the mockReturnValue
Jest mock method. It sets the return value of the stub.
The status
and json
methods on our mock response instance (res
) return the response instance (res
) itself.
const sinon = require('sinon');
const mockResponse = () => {
const res = {};
res.status = sinon.stub().returns(res);
res.json = sinon.stub().returns(res);
return res;
};
Testing a handler that reads from req
and sends a res
using status and json()
The checkAuth
handler reads from req
and sends a res
using status()
and json()
.
It contains the following logic, if session.data
is not set, the session is not set, and therefore the user is not authenticated, therefore it sends a 401 Unauthorized
status with an empty JSON body.
Otherwise, it reflects the part of the session contents (just the username
) in JSON response with a 200 status code.
Here’s the code under test (in express-handlers.js):
async function checkAuth(req, res) {
if (!req.session.data) {
return res.status(401).json();
}
const { username } = req.session.data;
return res.status(200).json({ username });
}
We need to test two paths: the one leading to a 401 and the other, leading to a 200.
See a snapshot of this code on GitHub github.com/HugoDF/mock-express-request-response/releases/tag/check-auth-tests (click on the commit sha for the diff for that version change).
Using the mockRequest
and mockResponse
we’ve defined before, we’ll set a request that has no session data (for 401) and does have session data containing username (for 200). Then we’ll check that req.status
is called with 401 and 200 respectively. In the 200 case we’ll also check that res.json
is called with the right payload ({ username }
).
In Jest (see express-handlers.jest-test.js):
describe('checkAuth', () => {
test('should 401 if session data is not set', async () => {
const req = mockRequest();
const res = mockResponse();
await checkAuth(req, res);
expect(res.status).toHaveBeenCalledWith(401);
});
test('should 200 with username from session if session data is set', async () => {
const req = mockRequest({ username: 'hugo' });
const res = mockResponse();
await checkAuth(req, res);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({ username: 'hugo' });
});
});
The same tests using sinon + AVA (in express-handlers.sinon-test.js):
test('checkAuth > should 401 if session data is not set', async (t) => {
const req = mockRequest();
const res = mockResponse();
await checkAuth(req, res);
t.true(res.status.calledWith(401));
});
test('checkAuth > should 200 with username from session if data is set', async (t) => {
const req = mockRequest({ username: 'hugo' });
const res = mockResponse();
await checkAuth(req, res);
t.true(res.status.calledWith(200));
t.true(res.json.calledWith({ username: 'hugo' }));
});
See a snapshot of this code on GitHub github.com/HugoDF/mock-express-request-response/releases/tag/check-auth-tests (click on the commit sha for the diff for that version change).
Testing a handler that writes to req
and sends a res
using status and json()
The logout
handler writes to req (it sets req.session.data
to null
) and sends a response using res.status
and res.json
. Here’s the code under test.
async function logout(req, res) {
req.session.data = null;
return res.status(200).json();
}
It doesn’t have any branching logic, but we should test that session.data
is reset and a response is sent in 2 separate tests. See a snapshot of this code on GitHub github.com/HugoDF/mock-express-request-response/releases/tag/logout-tests (click on the commit sha for the diff for that version change).
In Jest, with the mockRequest
and mockResponse
functions (in express-handlers.jest-test.js):
describe('logout', () => {
test('should set session.data to null', async () => {
const req = mockRequest({ username: 'hugo' });
const res = mockResponse();
await logout(req, res);
expect(req.session.data).toBeNull();
});
test('should 200', async () => {
const req = mockRequest({ username: 'hugo' });
const res = mockResponse();
await logout(req, res);
expect(res.status).toHaveBeenCalledWith(200);
});
});
In AVA + sinon using mockRequest and mockResponse functions (in express-handlers.sinon-test.js):
test('logout > should set session.data to null', async (t) => {
const req = mockRequest({ username: 'hugo' });
const res = mockResponse();
await logout(req, res);
t.is(req.session.data, null);
});
test('logout > should 200', async (t) => {
const req = mockRequest({ username: 'hugo' });
const res = mockResponse();
await logout(req, res);
t.true(res.status.calledWith(200));
});
See a snapshot of this code on GitHub github.com/HugoDF/mock-express-request-response/releases/tag/logout-tests (click on the commit sha for the diff for that version change).
A complex handler request/response mocking scenario: a request to login with a body
Our login handler does the heaviest lifting in the application. It’s in express-handlers.js
and containts the following logic.
The login handler first validates that the contents of req.body
and 400s if either of them are missing (this will be our first 2 tests).
The login handler then attempts to getUser
for the given username, if there is no such user, it 401s (this will be our 3rd test).
Next the login handler compares the password from the request with the hashed/salted version coming from getUser
output, if that comparison fails, it 401s (this will be our 4th test).
Finally, if the username/password are valid for a user, the login handler sets session.data to { username }
and sends a 201 response (this will be our 5th test).
The final test (that I haven’t implemented) that would make sense is to check that the handler sends a 500 if an error occurs during its execution (eg. getUser
throws).
See a snapshot of the code on GitHub github.com/HugoDF/mock-express-request-response/releases/tag/login-tests (click on the commit sha for the diff for that version change).
The login functions is as follows, for readability’s sake, I’ve omitted getUser
. getUser
is implemented as a hard-coded array lookup in any case whereas in your application it will be a database or API call of some sort (unless you’re using oAuth).
const bcrypt = require('bcrypt');
async function login(req, res) {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ message: 'username and password are required' });
}
const user = getUser(username);
if (!user) {
return res.status(401).json({ message: 'No user with matching username' });
}
if (!(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ message: 'Wrong password' });
}
req.session.data = { username };
return res.status(201).json();
} catch (e) {
console.error(`Error during login of "${req.body.username}": ${e.stack}`);
res.status(500).json({ message: e.message });
}
}
It’s consumed, by being “mounted” on the Express app in app.js
:
app.post('/session', login);
To be able to test the login function we need to extends the mockRequest
function, it’s still returning a plain JavaScript object so there is not difference between our Jest and AVA + sinon version:
const mockRequest = (sessionData, body) => ({
session: { data: sessionData },
body,
});
See a snapshot of the code on GitHub github.com/HugoDF/mock-express-request-response/releases/tag/login-tests (click on the commit sha for the diff for that version change).
Tests for login handler using in Jest
Note: There’s a big wall of code incoming.
You can skip to the sinon + AVA version if that’s what you’re interested in using this link
Skip to the Middleware and request.get headers section using this link.
To test this Express handler thoroughly is a few more tests but fundamentally the same principles as in the checkAuth
and logout
handlers.
The tests look like the following (in express-handlers.jest-test.js):
describe('login', () => {
test('should 400 if username is missing from body', async () => {
const req = mockRequest(
{},
{ password: 'boss' }
);
const res = mockResponse();
await login(req, res);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({
message: 'username and password are required'
});
});
test('should 400 if password is missing from body', async () => {
const req = mockRequest(
{},
{ username: 'hugo' }
);
const res = mockResponse();
await login(req, res);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({
message: 'username and password are required'
});
});
test('should 401 with message if user with passed username does not exist', async () => {
const req = mockRequest(
{},
{
username: 'hugo-boss',
password: 'boss'
}
);
const res = mockResponse();
await login(req, res);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({
message: 'No user with matching username'
});
});
test('should 401 with message if passed password does not match stored password', async () => {
const req = mockRequest(
{},
{
username: 'guest',
password: 'not-good-password'
}
);
const res = mockResponse();
await login(req, res);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({
message: 'Wrong password'
});
});
test('should 201 and set session.data with username if user exists and right password provided', async () => {
const req = mockRequest(
{},
{
username: 'guest',
password: 'guest-boss'
}
);
const res = mockResponse();
await login(req, res);
expect(res.status).toHaveBeenCalledWith(201);
expect(res.json).toHaveBeenCalled();
expect(req.session.data).toEqual({
username: 'guest',
});
});
});
See a snapshot of the code on GitHub github.com/HugoDF/mock-express-request-response/releases/tag/login-tests (click on the commit sha for the diff for that version change).
Tests for login handler using AVA + sinon
Note: There’s (another) big wall of code incoming.
You can go back to the Jest version if that’s what you’re interested in using this link
Skip to the Middleware and request.get headers section using this link.
Again there’s nothing fundamentally new in these tests, they’re just denser and closer to what you would do in a real-world application, they are as follows (in express-handlers.sinon-test.js):
See a snapshot of the code on GitHub github.com/HugoDF/mock-express-request-response/releases/tag/login-tests (click on the commit sha for the diff for that version change).
test('login > should 400 if username is missing from body', async (t) => {
const req = mockRequest(
{},
{ password: 'boss' }
);
const res = mockResponse();
await login(req, res);
t.true(res.status.calledWith(400));
t.true(res.json.calledWith({
message: 'username and password are required'
}));
});
test('should 400 if password is missing from body', async (t) => {
const req = mockRequest(
{},
{ username: 'hugo' }
);
const res = mockResponse();
await login(req, res);
t.true(res.status.calledWith(400));
t.true(res.json.calledWith({
message: 'username and password are required'
}));
});
test('should 401 with message if user with passed username does not exist', async (t) => {
const req = mockRequest(
{},
{
username: 'hugo-boss',
password: 'boss'
}
);
const res = mockResponse();
await login(req, res);
t.true(res.status.calledWith(401));
t.true(res.json.calledWith({
message: 'No user with matching username'
}));
});
test('should 401 with message if passed password does not match stored password', async (t) => {
const req = mockRequest(
{},
{
username: 'guest',
password: 'not-good-password'
}
);
const res = mockResponse();
await login(req, res);
t.true(res.status.calledWith(401));
t.true(res.json.calledWith({
message: 'Wrong password'
}));
});
test('should 201 and set session.data with username if user exists and right password provided', async (t) => {
const req = mockRequest(
{},
{
username: 'guest',
password: 'guest-boss'
}
);
const res = mockResponse();
await login(req, res);
t.true(res.status.calledWith(201));
t.true(res.json.called);
t.deepEqual(
req.session.data,
{ username: 'guest' }
);
});
See a snapshot of the code on GitHub github.com/HugoDF/mock-express-request-response/releases/tag/login-tests (click on the commit sha for the diff for that version change).
Testing a middleware and mocking Express request.get headers
Another scenario in which you might want to mock/stub the Express request and response objects is when testing a middleware function.
Testing middleware is subtly different. A lot of middleware has conditions under which it does nothing (just calls next()
). An Express middleware should always call next()
(its 3rd parameter) or send a response.
Here’s an example middleware which allows authentication using an API key in an Authorization
header of the format Bearer {API_KEY}
.
Beyond the middleware vs handler differences, headerAuth
is also using req.get()
, which is used to get headers from the Express request.
I’ve omitted apiKeyToUser
and isApiKey
. apiKeyToUser
is just a lookup from apiKeys to usernames. In a real-world application this would be a database lookup much like what would replace getUser
in the login
code.
function headerAuth(req, res, next) {
if (req.session.data) {
return next()
}
const authenticationHeader = req.get('authorization')
if(!authenticationHeader) {
return next()
}
const apiKey = authenticationHeader
.replace('Bearer', '')
.trim();
if (!isApiKey(apiKey)) {
return next()
}
req.session.data = { username: apiKeyToUser[apiKey] };
next();
}
See a snapshot of the code on GitHub github.com/HugoDF/mock-express-request-response/releases/tag/middleware-header-tests (click on the commit sha for the diff for that version change).
Updating mockRequest to support accessing headers
Here is a different version of mockRequest, it’s still a plain JavaScript object, and it mock req.get
just enough to get the tests passing:
const mockRequest = (authHeader, sessionData) => ({
get(name) {
if (name === 'authorization') return authHeader
return null
},
session: { data: sessionData }
});
Testing a middleware that accesses headers with Jest
Most of the tests check that nothing changes on the session while the middleware executes since it has a lot of short-circuit conditions.
Note how we pass a no-op function () => {}
as the 3rd parameter (which is next
).
describe('headerAuthMiddleware', () => {
test('should set req.session.data if API key is in authorization and is valid', async () => {
const req = mockRequest('76b1e728-1c14-43f9-aa06-6de5cbc064c2');
const res = mockResponse();
await headerAuthMiddleware(req, res, () => {});
expect(req.session.data).toEqual({ username: 'hugo' });
});
test('should not do anything if req.session.data is already set', async () => {
const req = mockRequest('76b1e728-1c14-43f9-aa06-6de5cbc064c2', { username: 'guest' });
const res = mockResponse();
await headerAuthMiddleware(req, res, () => {});
expect(req.session.data).toEqual({ username: 'guest' });
});
test('should not do anything if authorization header is not present', async () => {
const req = mockRequest(undefined);
const res = mockResponse();
await headerAuthMiddleware(req, res, () => {});
expect(req.session.data).toBeUndefined();
});
test('should not do anything if api key is invalid', async () => {
const req = mockRequest('invalid-api-key');
const res = mockResponse();
await headerAuthMiddleware(req, res, () => {});
expect(req.session.data).toBeUndefined();
});
});
See a snapshot of the code on GitHub github.com/HugoDF/mock-express-request-response/releases/tag/middleware-header-tests (click on the commit sha for the diff for that version change).
Testing a middleware that accesses headers using AVA + sinon
Most of the tests check that nothing changes on the session while the middleware executes since it has a lot of short-circuit conditions.
Note how we pass a no-op function () => {}
as the 3rd parameter (which is next
).
test('should set req.session.data if API key is in authorization and is valid', async (t) => {
const req = mockRequest('76b1e728-1c14-43f9-aa06-6de5cbc064c2');
const res = mockResponse();
await headerAuthMiddleware(req, res, () => {});
t.deepEqual(
req.session.data,
{ username: 'hugo' }
);
});
test('should not do anything if req.session.data is already set', async (t) => {
const req = mockRequest('76b1e728-1c14-43f9-aa06-6de5cbc064c2', { username: 'guest' });
const res = mockResponse();
await headerAuthMiddleware(req, res, () => {});
t.deepEqual(
req.session.data,
{ username: 'guest' }
);
});
test('should not do anything if authorization header is not present', async (t) => {
const req = mockRequest(undefined);
const res = mockResponse();
await headerAuthMiddleware(req, res, () => {});
t.is(req.session.data, undefined);
});
test('should not do anything if api key is invalid', async (t) => {
const req = mockRequest('invalid-api-key');
const res = mockResponse();
await headerAuthMiddleware(req, res, () => {});
t.is(req.session.data, undefined);
});
See a snapshot of the code on GitHub github.com/HugoDF/mock-express-request-response/releases/tag/middleware-header-tests (click on the commit sha for the diff for that version change).
Keys to testing Express handlers and middleware
There are a few keys to testing Express effectively in the manner outlined in this post.
First of all is understanding what the code does. It’s harder than it seems. Testing in JavaScript is a lot about understanding JavaScript, a bit about testing tools and a bit understanding the tools used in that application under test. In order to mock the tool’s return values with the right type of data.
All the tests in the post boil down to understanding what req
, res
and next
are (an object, an object and a function). Which properties they have/can have, how those properties are used and whether they’re a function or an object.
This is only 1 approach to testing Express handlers and middleware. The alternative is to fire up the Express server (ideally in-memory using SuperTest). I go into more detail on how to achieve that in “Testing an Express app with SuperTest, moxios and Jest”
Get The Jest Handbook (100 pages)
Take your JavaScript testing to the next level by learning the ins and outs of Jest, the top JavaScript testing library.
orJoin 1000s of developers learning about Enterprise-grade Node.js & JavaScript