Supertest与Sinon:同一测试文件中多次存根同一函数失败问题
问题:Supertest结合Sinon多次存根同一函数失败的原因与解决方法
我无法在同一测试文件中结合Supertest与Sinon对同一函数进行多次存根操作,请问该如何实现?
以下是我当前的代码:
const { app } = require('../../app'); const request = require('supertest'); const sinon = require('sinon'); const { expect } = require('chai'); const testConfig = require('../testConfig/testConfig'); const utils = require('../../helpers/utils'); const token = testConfig.token; const header = { 'Authorization': `Bearer ${token}` }; const mockOrder = { accountNumber: '1234', orderSourceId: 1 } const mockOrders = [mockOrder]; describe('Vendors routes', () => { afterEach(() => { sinon.restore(); }) describe('GET /orders/:accountNumber', () => { it('should get an order record for provided Account Number', async () => { sinon.stub(utils, 'getOrder').resolves(mockOrders); const accountNumber = '1234'; const route = testConfig.createRoute(`/vendors/orders/${accountNumber}`); const res = await request(app) .get(route) .set(header); expect(res.status).to.be.equal(200); }) it('should return 404 if an order is not found', async () => { sinon.stub(utils, 'getOrder').resolves(undefined); const accountNumber = '1234'; const route = testConfig.createRoute(`/vendors/orders/${accountNumber}`); const res = await request(app) .get(route) .set(header); expect(res.status).to.be.equal(404); sinon.restore(); }) }) })
运行时出现以下错误:
Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.
第一个测试用例正常通过,但第二个测试用例失败。为什么Sinon无法覆盖已存根的函数?具体原因是什么?
问题原因
- 重复恢复存根:你已经在
afterEach钩子中配置了自动恢复所有Sinon存根,但第二个测试用例里又手动调用了sinon.restore()。这会导致测试逻辑还在执行时(比如Supertest的请求还没处理完),存根就被提前恢复,路由处理时调用的是原utils.getOrder函数而非存根版本。如果原函数是异步操作且没有被正确模拟,就会导致请求超时。 - Node.js模块缓存:如果
app在测试文件顶部就被导入,而app内部已经引用了utils模块,那么Node.js会缓存utils的实例。第一次存根修改的是这个缓存实例,但后续测试的存根可能无法作用到app实际使用的utils引用上,导致存根失效。
解决方法
方法一:移除测试用例内的手动恢复
保留afterEach的自动恢复即可,避免提前破坏存根:
修改第二个测试用例,删除末尾的sinon.restore():
it('should return 404 if an order is not found', async () => { sinon.stub(utils, 'getOrder').resolves(undefined); const accountNumber = '1234'; const route = testConfig.createRoute(`/vendors/orders/${accountNumber}`); const res = await request(app) .get(route) .set(header); expect(res.status).to.be.equal(404); // 删掉这里的sinon.restore() })
方法二:每次测试重新加载app
针对模块缓存问题,可以在每个测试前清除app的模块缓存并重新导入,确保app使用最新的utils存根:
// 移除文件顶部的const { app } = require('../../app'); describe('Vendors routes', () => { let app; beforeEach(() => { // 清除app的模块缓存 delete require.cache[require.resolve('../../app')]; // 重新导入app app = require('../../app').app; }) afterEach(() => { sinon.restore(); }) // 测试用例保持不变 })
方法三:针对单个存根进行操作
如果需要更精细的控制,可以只恢复当前测试用例的存根,而非全局恢复:
it('should get an order record for provided Account Number', async () => { // 保存存根实例 const getOrderStub = sinon.stub(utils, 'getOrder').resolves(mockOrders); const accountNumber = '1234'; const route = testConfig.createRoute(`/vendors/orders/${accountNumber}`); const res = await request(app) .get(route) .set(header); expect(res.status).to.be.equal(200); // 仅恢复这个存根 getOrderStub.restore(); })
内容的提问来源于stack exchange,提问作者Robert
相关产品推荐
相关产品推荐

