Search code examples
node.jsexpressjestjsintegration-testingsupertest

What is the correct order of requiring and mocking files using Jest?


I'm trying to create an integration test using Jest for my Express app. I think I have a conceptual misunderstanding as my tests are behaving strangely. My goal is to test the following scenario. I'm hitting a specific endpoint using Supertest, and I want to check whether an error handler middleware is called if there is a mocked error. I want to check whether the error handler is not called, if there is no error present. I have the following test file:

test.js

const request = require('supertest')

describe('Error handler', () => {
  let server
  let router

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

  afterEach(async () => {
    await server.close()
  })

  it('should be triggered if there is a router error', async () => {  
    jest.mock('../../routes/')
    router = require('../../routes/')
  
    router.mockImplementation(() => {
      throw new Error()
    })
  
    server = require('../../server')

    const res = await request(server)
      .get('')
      .expect(500)
      .expect('Content-Type', /json/)
  
    expect(res.body.error).toBe('Error')
    expect(res.body.message).toBe('Something went wrong!')
    expect(res.body.status).toBe(500 )  
  })

  it('should not be triggered if there is no router error', async () => {  
    server = require('../../server')
    
    const res = await request(server)
      .get('')
      .expect(201)
      .expect('Content-Type', /text/)
  })

})

What I think is happening is the following. Before each test I reset all modules, because I don't want to have the cached version of my server from the first require, I want to overwrite it. I also reset all mocks, so when the second test runs, no mock is used, no fake error is forced, so the middleware is not called and I'm getting back a vanilla 200 result.

After this is done, I start testing the scenario when there is an error. I mock the routes file that exports my routes so I can force a fake error. Then I require the server, this way, I suppose, it's loading the server up with the fake, error throwing route. Then I wait for the response with Supertest, and assert that I indeed got an error back - hence the error handler middleware has been triggered and worked.

The afterEach hook is called, the server is closed, then the beforeEach hook initializes everything, again. Now I have my vanilla implementation without the mock. I require my server, hit the homepage with a get request, and I get back the correct response.

The strange thing is that for some reason the second test seems to not exit gracefully. If I change my implementation from async - await in the second test, to specify the done callback, and then if I call it at the end of the test, it seems to be working.

I tried a lot of possible permutations, including putting the mocking part to the beforeEach hook, starting the server before / after mocking, and I got weird results. I feel like I have conceptual misunderstandings, but I don't know where, because there are so many moving parts.

Any help to make me understand what is wrong would be greatly appreciated

EDIT:

I thought that most parts can be considered a black box, but now I realize that the fact that I'm trying to create an app using Socket.IO makes the setup process a bit more convoluted.

I don't want Express to automatically create a server for me, because I want to use socketIO. So for now I only create a function with the appropiate signature, and that is 'app'. This can be given as an argument to http.Server(). I configure it with options and the middlewares that I want to use. I do not want to call app.listen, because that way Socket.IO could not do its own thing.

config.js

const path = require('path')
const express = require('express')
const indexRouter = require('./routes/')
const errorHandler = require('./middlewares/express/errorHandler')

const app = express()

app.set('views', path.join(__dirname + '/views'))
app.set('view engine', 'ejs')

app.use(express.static('public'))
app.use('', indexRouter)
app.use(errorHandler)

module.exports = app

In server.js I require this app, and then I create a HTTP server using it. After that, I feed it to 'socket.io', so it is connected to the proper instance. In server.js I do not call server.listen, I want to export it to a file that actually starts up the server (index.js) and I want to export it to my tests, so Supertest can spin it up.

server.js

// App is an Express server set up to use specific middlewares
const app = require('./config')
// Create a server instance so it can be used by to SocketIO
const server = require('http').Server(app)
const io = require('socket.io')(server)
const logger = require('./utils/logger')
const Game = require('./service/game')
const game = new Game()

io.on('connection', (socket) => {
  logger.info(`There is a new connection! Socket ID: ${socket.id}`)
  
  // If this is the first connection in the game, start the clock
  if (!game.clockStarted) {
    game.startClock(io)
    game.clockStarted = true
  }
  
  game.addPlayer(socket)
  
  socket.on('increaseTime', game.increaseTime.bind(game))
})

module.exports = server

If I understand everything correctly, basically the same thing happens, expect for a few additional steps in the example that you provided. There is no need to start the server, and then use Supertest on it, Supertest handles the process of starting up the server when I use request(server).get, etc.

EDIT 2

Right now I'm not sure whether mocking like that is enough. Some mysterious things leaves the Supertest requests hanging, and it might be that somewhere along the way it can not be ended, although I do not see why would that be the case. Anyway, here is the router:

routes/index.js

const express = require('express')
const router = express.Router()

router.get('', (req, res, next) => {
  try {
    res.status(200).render('../views/')
  } catch (error) {
    next(error)
  }
})

router.get('*', (req, res, next) => {
  try {
    res.status(404).render('../views/not-found')
  } catch (error) {
    next(error)
  }  
})

module.exports = router

Solution

  • The order of requiring and mocking is correct but the order of setting up and shutting down a server probably isn't.

    A safe way is to make sure the server is available before doing requests. Since Node http is asynchronous and callback-based, errors cannot be expected to be handled in async functions without promisification. Considering that server.listen(...) was called in server.js, it can be:

    ...
    server = require('../../server')
    expect(server.listening).toBe(true);
    await new Promise((resolve, reject) => {
      server.once('listening', resolve).once('error', reject);
    });
    const res = await request(server)
    ...
    

    close is asynchronous and doesn't return a promise so there's nothing to await. Since it's in a dedicated block, a short way is to use done callback:

    afterEach(done => {
      server.close(done)
    })
    

    In case errors are suppressed in error listener, server.on('error', console.error) can make troubleshooting easier.

    Supertest can handle server creation itself:

    You may pass an http.Server, or a Function to request() - if the server is not already listening for connections then it is bound to an ephemeral port for you so there is no need to keep track of ports.

    And can be provided with Express instance instead of Node server, this eliminates the need to handle server instances manually:

    await request(app)
    ...