6.1 Mock request/response objects in-memory
When writing unit tests for an Express application, using mock request and response objects is simple and to the point.
Mock request/response objects in-memory
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.
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). We go into detail for that in the next section.
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;
};
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 });
}
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);
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.
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(request, res) {
if (!request.session.data) {
return res.status(401).json();
}
const {username} = request.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.
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 }
).
test('should 401 if session data is not set', async () => {
const request = mockRequest();
const res = mockResponse();
await checkAuth(request, res);
expect(res.status).toHaveBeenCalledWith(401);
});
test('should 200 with username from session if session data is set', async () => {
const request = mockRequest({username: 'hugo'});
const res = mockResponse();
await checkAuth(request, res);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({username: 'hugo'});
});
The tests pass, validating our functionality.
npx jest src/06.01-check-auth.test.js
PASS src/06.01-check-auth.test.js
✓ should 401 if session data is not set (4ms)
✓ should 200 with username from session if session data is set (1ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
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(request, res) {
request.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.
In Jest, with the mockRequest
and mockResponse
functions (in express-handlers.jest-test.js):
test('should set session.data to null', async () => {
const request = mockRequest({username: 'hugo'});
const res = mockResponse();
await logout(request, res);
expect(request.session.data).toBeNull();
});
test('should 200', async () => {
const request = mockRequest({username: 'hugo'});
const res = mockResponse();
await logout(request, res);
expect(res.status).toHaveBeenCalledWith(200);
});
The output is as follows.
npx jest src/06.01-logout.test.js
PASS src/06.01-logout.test.js
✓ should set session.data to null (3ms)
✓ should 200 (1ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
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).
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).
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 (error) {
console.error(
`Error during login of "${req.body.username}": ${error.stack}`
);
res.status(500).json({message: error.message});
}
}
To be able to test the login function we need to extends the mockRequest
function, it’s still returning a plain JavaScript object:
const mockRequest = (sessionData, body) => ({
session: { data: sessionData },
body
});
Tests for login handler using in Jest
Note: There’s a big wall of code incoming.
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):
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'
});
});
Test output is as follows.
npx jest src/06.01-login.test.js
PASS src/06.01-login.test.js
✓ should 400 if username is missing from body (5ms)
✓ should 400 if password is missing from body (1ms)
✓ should 401 with message if user with passed username does not exist (2ms)
✓ should 401 with message if passed password does not match stored password (103ms)
✓ should 201 and set session.data with username if user exists and right password provided (95ms)
Test Suites: 1 passed, 1 total
Tests: 5 passed, 5 total
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();
}
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
).
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 headerAuth(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 headerAuth(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 headerAuth(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 headerAuth(req, res, () => {});
expect(req.session.data).toBeUndefined();
});
Test output is as follows.
npx jest src/06.01-header-auth.test.js
PASS src/06.01-header-auth.test.js
✓ should set req.session.data if API key is in authorization and is valid (9ms)
✓ should not do anything if req.session.data is already set (1ms)
✓ should not do anything if authorization header is not present (3ms)
✓ should not do anything if api key is invalid (1ms)
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
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). The next section deals with using that approach.