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.

Jump to table of contents