使用 Jest 要求和模拟文件的正确顺序是什么?

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

我正在尝试使用 Jest 为我的 Express 应用程序创建集成测试。我认为我有一个概念上的误解,因为我的测试表现得很奇怪。我的目标是测试以下场景。我正在使用 Supertest 访问特定端点,并且我想检查是否在出现模拟错误时调用错误处理程序中间件。如果不存在错误,我想检查是否未调用错误处理程序。我有以下测试文件:

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/)
  })

})

我认为正在发生的事情如下。在每次测试之前,我都会重置所有模块,因为我不想从第一个 require 中获得我的服务器的缓存版本,我想覆盖它。我还重置了所有模拟,所以当第二次测试运行时,没有使用模拟,没有强制伪造错误,所以中间件没有被调用,我得到了一个普通的 200 结果。

完成后,我开始测试出现错误时的场景。我模拟了导出我的路线的路线文件,这样我就可以强制伪造错误。然后我需要服务器,我想,它正在用虚假的错误抛出路由加载服务器。然后我等待 Supertest 的响应,并断言我确实收到了错误返回 - 因此错误处理程序中间件已被触发并工作。

调用 afterEach 挂钩,关闭服务器,然后 beforeEach 挂钩再次初始化所有内容。现在我有了没有模拟的普通实现。我需要我的服务器,用获取请求访问主页,我得到了正确的响应。

奇怪的是,由于某种原因,第二个测试似乎没有正常退出。如果我在第二次测试中将我的实现从 async - await 更改为指定 done 回调,然后如果我在测试结束时调用它,它似乎可以正常工作。

我尝试了很多可能的排列,包括将模拟部分放在 beforeEach 挂钩中,在模拟之前/之后启动服务器,但我得到了奇怪的结果。感觉自己有概念上的误区,但又不知道是哪里,因为活动的部分太多了。

任何帮助我理解错误的帮助都将不胜感激

编辑:

我认为大多数部分都可以被认为是一个黑盒子,但现在我意识到我正在尝试使用 Socket.IO 创建一个应用程序这一事实使得设置过程有点复杂。

我不希望 Express 自动为我创建服务器,因为我想使用 socketIO。所以现在我只创建一个具有适当签名的函数,即 'app'。这可以作为 http.Server() 的参数给出。我用选项和我想使用的中间件配置它。我不想调用 app.listen,因为那样 Socket.IO 不能做它自己的事情。

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

在 server.js 中,我需要这个应用程序,然后我使用它创建了一个 HTTP 服务器。之后,我将它提供给 'socket.io',因此它连接到正确的实例。在server.js我不调用server.listen,我想把它导出到一个实际启动服务器的文件(index.js),我想把它导出到我的测试中,所以Supertest可以旋转起来。

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

如果我理解正确,基本上会发生同样的事情,期待您提供的示例中的一些额外步骤。不需要启动服务器,然后在上面使用Supertest,Supertest处理我使用request(server).get等时启动服务器的过程

编辑 2

现在我不确定这样的嘲笑是否足够。一些神秘的事情让 Supertest 请求悬而未决,可能是在某个地方无法结束,尽管我不明白为什么会这样。无论如何,这是路由器:

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

要求和模拟的顺序是正确的,但设置和关闭服务器的顺序可能不正确。

一种安全的方法是在发出请求之前确保服务器可用。由于节点 http 是异步的并且 callback-based,因此不能指望在 async 函数中处理错误而无需 promisification。考虑到在server.js中调用了server.listen(...),可以是:

...
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 是异步的,return 不是一个承诺,所以 await 没有什么。由于它位于专用块中,因此一种简短的方法是使用 done 回调:

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

如果 error 侦听器中的错误被抑制,server.on('error', console.error) 可以使故障排除更容易。

Supertest 可以自己处理服务器创建:

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.

并且可以提供 Express 实例而不是 Node 服务器,这样就无需手动处理服务器实例:

await request(app)
...