Search code examples
node.jsexpressjestjssupertest

jest.setTimeout.Error: Mocking Express middleware with Jest and Supertest


I would like to mock the auth middleware function to always just call next(). To try and acheive this, I added the following to the beginning of my test file before the auth middleware function is added to the app in app.js.

jest.mock('../../middleware/auth.js', () =>
  // ensure func is mocked before being attached to the app instance
  jest.fn((req, res, next) => next()) // seems to only work for first request that hits this middleware
); // Mock authentication

I then added some debugs in the auth middleware but did not hit them for any of the tests.

Currently I am using the following, when the beforeEach() function is not commented out the tests all pass:

role.test.js

jest.mock('../../middleware/auth.js', () =>
  // ensure func is mocked before being attached to the app instance
  jest.fn((req, res, next) => next()) // seems to only work for first request that hits this middleware
); // Mock authentication

const request = require('supertest');
const app = require('../../app.js');
const Role = require('../../../db/models/index.js').Role;
// const auth = require('../../middleware/auth.js');
/**
 * Test role API routes
 */
describe('role-router', () => {
  let res;
  const token = 'valid-token';
  const baseUrl = '/private/api/roles';

  // Without the `beforeEach()` only the first request sent using supertest will use the mocked functionality
  // With the `beforeEach()` everything functions as expected, but why?
  // beforeEach(() => {
  //   auth.mockImplementation((req, res, next) => next());
  // });

  describe(`Create new role | post ${baseUrl}`, () => {
    describe('When successful', () => {
      beforeAll(async () => {
        // this will use the proper mocked functionality
        res = await request(app)
          .post(baseUrl)
          .set('Authorization', token)
          .send({
            roleName: 'Secret Agent',
            roleDesc: "World's best secret agent",
            ...
          });
      });

      // passes
      it('Should return 201', () => {
        expect(res.status).toBe(201);
      });
    }); // When successful
  }); // Create new role

  describe(`Delete role by id | delete ${baseUrl}/:id`, () => {
    describe('When successful', () => {
      beforeAll(async () => {
        const role = await Role.create({
          roleName: 'Secret Agent',
          roleDesc: "World's best secret agent",
          ...
        });
        
        // fails does not get response, res remains the same as res from previous test
        res = await request(app)
          .delete(`${baseUrl}/${role.id}`)
          .set('Authorization', token)
          .send();
      });

      // fails with 201
      it('Should return 204', () => {
        expect(res.status).toBe(204);
      });
    }); // When successful
  }); // Delete role
}); 

Error received:

 ● role-router › Delete role by id | delete /private/api/roles/:id › When successful › Should return 204

    Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error: Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.

      at mapper (node_modules/jest-jasmine2/build/queueRunner.js:29:45)

app.js

// Dependencies
const express = require('express');
const auth = require('./middleware/auth.js');
...

/**
 * Express application
 */
const app = express();

// Middleware
app.use(express.json());
...

// Private routes are secured with jwt authentication middleware
app.all('/private/*', (req, res, next) => auth(req, res, next));

// Public routes
...

// Private routes
app.use('/private/api/roles/', require('./components/role/role-router.js'));
...

module.exports = app;

Any ideas why this is not working without using the beforeEach() function to mock the middleware functionality before every test? Perhaps I am missing something more serious?


Solution

  • The reason for such behaviour is that there is spy.mockReset(), jest.resetAllMocks() or enabled resetMocks configuration option. Use restoreMocks option instead for a reasonable default configuration.

    The difference between jest.fn(() => ...) and jest.fn().mockImplementation(() => ...) is how they respond to jest.restoreAllMocks(). restoreAllMocks restores original implementation which is a no-op in case of jest.fn(), while () => ... is considered original implementation in case of jest.fn(...). So normally jest.fn(() => ...) implementation isn't expected to be reset.

    The difference between jest.resetAllMocks and jest.restoreAllMocks is that the former can make jest.fn(...) a noop but doesn't restore spies to original implementations. This specifically affects spies that are provided in mocked modules once per test suite with jest.mock, yet still doesn't restore global spies. Since this behaviour is rarely desirable, jest.restoreAllMocks() or restoreMocks configuration option are generally preferable. In case there's ever a need for jest.fn(...) implementation to be reset, mockReset or mockImplementation can be done specifically for this spy.