首页 > 解决方案 > 使用 Jest 要求和模拟文件的正确顺序是什么?

问题描述

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

测试.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/)
  })

})

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

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

afterEach 钩子被调用,服务器关闭,然后 beforeEach 钩子再次初始化一切。现在我有了没有模拟的香草实现。我需要我的服务器,通过获取请求访问主页,然后我得到正确的响应。

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

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

任何让我了解问题所在的帮助将不胜感激

编辑:

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

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

配置.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 可以启动它。

服务器.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 的请求悬而未决,并且可能在某个地方无法结束,尽管我不明白为什么会这样。无论如何,这是路由器:

路线/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

标签: node.jsexpressjestjsintegration-testingsupertest

解决方案


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

一种安全的方法是在发出请求之前确保服务器可用。由于 Nodehttp是异步且基于回调的,因此不能期望在没有承诺的情况下在async函数中处理错误。考虑到它server.listen(...)是在 server.js 中调用的,它可以是:

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

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

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

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

您可以将 http.Server 或 Function 传递给 request() - 如果服务器尚未侦听连接,则它会为您绑定到临时端口,因此无需跟踪端口。

并且可以提供 Express 实例而不是 Node 服务器,这消除了手动处理服务器实例的需要:

await request(app)
...

推荐阅读