Express Jest and Supertest how to mock middleware with argument
Asked Answered
B

3

7

I have been trying for several hours to test an endpoint of my rest api with Jest, Supertest and Express.

This endpoint is protected by an authentication middleware named "advancedAuthGuard".

So I'm trying to mock this middleware in order to skip authentication check for endpoints testing

//./router.js 
router.get('/route1', advancedAuthGuard(false), controller);

Important: advancedAuthGuard is a middleware that accepts configuration argument ( curried middleware )

//./middleware/advancedAuthGuard.js
const advancedAuthGuard = (additionalCondition) => (req,res,next) => {

  //Check authentication logic ...
  const isAuth = true

  if (isAuth && !additionalCondition)
    next()
  else
    next(new Error('Please auth'))
}

When I run the following test to check if I get status code '200' . The test fail before run.

//./test.js
import supertest from "supertest"
import app from './app.js'
import { advancedAuthGuard } from "./middlewares/advancedAuthGuard";

jest.mock("./middlewares/advancedAuthGuard")

const request = supertest(app)

beforeEach(()=>{
  jest.clearAllMocks()
})

it("should '/route1' respond with 200 status", async ()=>{
  const mockedMiddleware = jest.fn().mockImplementation((res, req, next)=> next() )
  advancedAuthGuard.mockReturnValue(mockedMiddleware)
  const res = await request.get("/route1")
  expect(res.status).toEqual(200)
})

I get the following error:

Test suite failed to run
Route.get() requires a callback function but got a [object Undefined]


> 10 | router.get('/route1', advancedAuthGuard(false), controller);
     |        ^

I therefore deduce that the problem comes from the mock...

And when I debug it via console.log, I realize that I get an incomplete mock :

  • I was able to verify that the mock of function (advancedAuthGuard(false)) was present
  • But this mock should return a second mock of type function (req,res,next){}, however it only returns undefined

I also noticed that:

  • This mock issue only occurs with curried middlewares (middleware with input parameters)
  • This mock issue only occurs when the curried middleware is executed in express router. (When I tried the mock outside express router, the mock appears correctly)

So I would like to know why it is impossible for me to mock a curried middleware ( middleware with argument ) used in an Expressjs endpoint with Jest and Supertest.

Here is a github link with minimal express, jest, supertest config, where you can reproduce this problem, by running the test.js file. https://github.com/enzo-cora/jest-mock-middleware

Bushire answered 29/8, 2022 at 0:28 Comment(1)
Did you ever find a suitable solution for this? I'm encountering the exact same issue.Kayceekaye
H
7

Not sure why it is not working the way you tried but if you pass the mock implementation to the autoMock function it will do the trick.

import supertest from "supertest";
import app from "./app";


jest.mock("./middlewares/simpleAuthGuard", () => ({
  simpleAuthGuard: (_, __, next) => next()
}));
jest.mock("./middlewares/advancedAuthGuard", () => ({
  advancedAuthGuard: () => (_, __, next) => next()
}));

const request = supertest(app);

describe('My tests', () => {
  beforeEach(() => jest.clearAllMocks());

  it("should '/route1' with advanced middleware work", async () => {
    const res = await request.get("/route1");
    expect(res.status).toEqual(200);
  });

  it("should '/route2' with simple middleware work", async () => {
    const res = await request.get("/route2")
    expect(res.status).toEqual(200)
  });
});

Husky answered 29/8, 2022 at 1:9 Comment(1)
Thank you, this allows at least to validate the test. BUT: However, I need to be able to control the implementation of my advancedAuthGuard through my various tests. (Sometime, i want send auth success, and other time I want send auth fail ) In your solution, the advancedAuthGuard works the same for all tests. So I could not create different authentication result scenarios in my test suit.Bushire
A
0

You can use jest.doMock(moduleName, factory, options) when you want to mock a module differently within the same file.

app.ts:

import express from 'express';
import { advancedAuthGuard } from './middlewares/advancedAuthGuard';

const app = express();

app.get('/route1', advancedAuthGuard(false), (req, res) => {
  res.sendStatus(200);
});

app.use((err, req, res, next) => {
  res.status(500).send('Something broke!');
});

export default app;

middlewares/advancedAuthGuard.ts:

export const advancedAuthGuard = (additionalCondition) => (req, res, next) => {
  const isAuth = true;

  if (isAuth && !additionalCondition) next();
  else next(new Error('Please auth'));
};

app.test.ts:

import supertest from 'supertest';

beforeEach(() => {
  jest.resetModules();
});

it("should '/route1' respond with 200 status", async () => {
  jest.doMock('./middlewares/advancedAuthGuard', () => {
    return {
      advancedAuthGuard: () => jest.fn().mockImplementation((res, req, next) => next()),
    };
  });
  const app = (await import('./app')).default;
  const res = await supertest(app).get('/route1');
  expect(res.status).toEqual(200);
});

it("should '/route1' respond with 500 status", async () => {
  jest.doMock('./middlewares/advancedAuthGuard', () => {
    return {
      advancedAuthGuard: () => jest.fn().mockImplementation((res, req, next) => next(new Error('test'))),
    };
  });
  const app = (await import('./app')).default;
  const res = await supertest(app).get('/route1');
  expect(res.status).toEqual(500);
  expect(res.text).toEqual('Something broke!');
});

Test result:

 PASS  stackoverflow/73523182/app.test.ts
  √ should '/route1' respond with 200 status (125 ms)
  √ should '/route1' respond with 500 status (45 ms)                                                                                                                                                                                           
                                                                                                                                                                                                                                               
Test Suites: 1 passed, 1 total                                                                                                                                                                                                                 
Tests:       2 passed, 2 total                                                                                                                                                                                                                 
Snapshots:   0 total
Time:        0.996 s, estimated 11 s
Ran all test suites related to changed files.
Aghast answered 19/6, 2024 at 4:34 Comment(0)
K
0

Having encountered this exact same issue today, and not finding any solutions that worked, I stumbled upon one that did work, and quite elegantly. Originally, I had setup my middleware the same as the OP:

router.post('/my/route', authMiddleware(requiredRoles),
  (req, res, next) => { /* other code */ },
);

Even though I was able to mock my curried function successfully, because I had several other routes in the same file, the authMiddleware function was getting executed once for each route when they were attached to supertest, which is not really what you want and makes it difficult to inspect what the proper args are for the route I was testing.

If, instead, you make a small change to the how you call the middleware:

router.post('/my/route',
  (req, res, next) => authMiddleware(requiredRoles)(req, res, next),
  (req, res, next) => { /* other code */ },
);

The middleware only gets executed when you hit the route, not when they get attached, which makes it infinitely easier to mock and verify arguments:

import supertest from 'supertest';
import { authMiddlewareMock } from './middleware/auth';
import { router } from './router';
jest.mock('./middleware/auth');
const authMiddlewareMock = jest.mocked(authMiddleware);
const fakeMiddleware = (req, res, next) => next();

describe('POST /my/route', () => {
  it('should do the right stuff', async () => {
    authMiddlewareMock.mockReturnValue(fakeMiddleware);
    const response = await supertest(router);
    expect(authMiddlewareMock).toHaveBeenCalledWith('admin');
  });
});
Kayceekaye answered 20/7, 2024 at 0:9 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.