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

Node.js 使用Jest要求和模拟文件的正确顺序是什么?,node.js,express,jestjs,integration-testing,supertest,Node.js,Express,Jestjs,Integration Testing,Supertest,我正在尝试使用Jest为我的Express应用程序创建集成测试。我想我有一个概念上的误解,因为我的测试表现得很奇怪。我的目标是测试以下场景。我正在使用Supertest访问一个特定的端点,我想检查如果存在模拟错误,是否调用了错误处理程序中间件。如果不存在错误,我想检查是否未调用错误处理程序。我有以下测试文件: test.js const request = require('supertest') describe('Error handler', () => { let serve

我正在尝试使用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/)
  })

})

我认为正在发生的事情如下。在每次测试之前,我都会重置所有模块,因为我不想让服务器的缓存版本从第一个需求开始,我想覆盖它。我还重置了所有mock,因此在第二次测试运行时,没有使用mock,也没有强制执行假错误,因此不会调用中间件,我将返回一个结果

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

调用每个钩子之后,服务器关闭,然后beforeach钩子再次初始化所有内容。现在我有了没有mock的普通实现。我需要我的服务器,点击带有get请求的主页,然后得到正确的响应

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

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

如果能帮助我了解问题所在,我将不胜感激

编辑:

我原以为大多数部分都可以看作是一个黑匣子,但现在我意识到,我正在尝试使用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
是异步的,并且基于回调,因此在没有提示的情况下,不能期望在
async
函数中处理错误。考虑到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
是异步的,不会返回承诺,因此无需等待。由于它位于一个专用块中,一个简单的方法是使用
done
callback:

afterEach(done => {
  server.close(done)
})
如果在
error
listener中抑制了错误,那么
server.on('error',console.error)
可以使故障排除更容易

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

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

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

await request(app)
...

这道菜看起来不错。resetAllMocks很糟糕,可以使用restoreAllMocks,但这不是问题。第二个测试似乎没有顺利退出——发生了什么?指定完成的回调,然后如果我在测试结束时调用它,它似乎正在工作。-你到底试了什么
Wait server.close()
-这是一个错误,
close
是异步的,但它基于回调且不返回承诺,不确定这是否导致问题,但它可以。尝试
服务器。关闭(完成)
。如果这解决了问题,那就是问题所在。@EstusFlask如果我让实现保持原样,我在第二次测试中得到一个错误,说“异步回调在5000ms内未被调用”和“工作进程未能正常执行”。所以我想第二次测试没有告诉我们应该完成。奇怪的是,如果我将async-Wait更改为香草回调,它看起来像