A guide to Express request and response mocking/stubbing with Jest or sinon

Hugo Di Francesco - Feb 18 '19 - - Dev Community

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:

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

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

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”

unsplash-logo
Chris Barbalis

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .