You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为Jest测试Stub异步模块依赖?超时问题排查求助

异步模块依赖的Jest测试Stub最佳实现方案

问题根源

你遇到的超时问题,核心是Stub返回的Promise处于永久pending状态——手动创建Promise时没有调用resolve或reject,导致await example(...)一直阻塞,最终触发Jest的超时限制。

两种最佳实现方案

方案一:用Sinon正确处理异步Stub

Sinon提供了专门的resolves和rejects方法,替代手动创建Promise,确保异步状态正确流转:

import * as exampleModule from '../some/place'
import { processRequest } from '../routes/processRequest'
import sinon from 'sinon'

describe('processRequest test', () => {
  let exampleStub: sinon.SinonStub

  // 每个测试前创建Stub,测试后恢复原始方法
  beforeEach(() => {
    exampleStub = sinon.stub(exampleModule, 'example').resolves('mock-processed-data')
  })

  afterEach(() => {
    exampleStub.restore()
  })

  it('should respond with 200 when example succeeds', async () => {
    // 模拟请求、响应、next函数
    const mockRequest = { body: { someData: 'input-test-data' } } as Request
    const mockResponse = { 
      status: sinon.stub().returnsThis(), 
      send: sinon.stub() 
    } as unknown as Response
    const mockNext = sinon.stub()

    await processRequest(mockRequest, mockResponse, mockNext)

    // 验证核心逻辑
    expect(exampleStub.calledWith('input-test-data')).toBe(true)
    expect(mockResponse.status.calledWith(200)).toBe(true)
    expect(mockResponse.send.called()).toBe(true)
    expect(mockNext.called()).toBe(false)
  })

  it('should call next with error when example fails', async () => {
    const testError = new Error('example-execution-failed')
    exampleStub.rejects(testError)

    const mockRequest = { body: { someData: 'invalid-data' } } as Request
    const mockResponse = { status: sinon.stub(), send: sinon.stub() } as unknown as Response
    const mockNext = sinon.stub()

    await processRequest(mockRequest, mockResponse, mockNext)

    expect(mockNext.calledWith(testError)).toBe(true)
    expect(mockResponse.status.called()).toBe(false)
  })
})

核心要点:

  • 用resolves/rejects替代手动创建Promise,避免pending状态
  • 通过beforeEach/afterEach管理Stub的生命周期,防止测试用例间污染
  • 增加断言验证Stub调用、响应方法执行等逻辑,确保测试完整性

方案二:直接使用Jest内置Mock功能(无需Sinon)

Jest本身内置了完善的Mock能力,写法更统一,无需额外依赖:

// 提前mock目标模块
jest.mock('../some/place', () => ({
  example: jest.fn()
}))

import { example } from '../some/place'
import { processRequest } from '../routes/processRequest'

describe('processRequest test', () => {
  it('should return 200 on successful example call', async () => {
    // 设置异步返回值
    (example as jest.Mock).mockResolvedValue('mock-result')

    const mockRequest = { body: { someData: 'test-input' } } as Request
    const mockResponse = { 
      status: jest.fn().mockReturnThis(), 
      send: jest.fn() 
    } as unknown as Response
    const mockNext = jest.fn()

    await processRequest(mockRequest, mockResponse, mockNext)

    expect(example).toHaveBeenCalledWith('test-input')
    expect(mockResponse.status).toHaveBeenCalledWith(200)
    expect(mockResponse.send).toHaveBeenCalled()
    expect(mockNext).not.toHaveBeenCalled()
  })

  it('should propagate error via next when example fails', async () => {
    const testError = new Error('mock-error')
    (example as jest.Mock).mockRejectedValue(testError)

    const mockRequest = { body: { someData: 'bad-input' } } as Request
    const mockResponse = { status: jest.fn(), send: jest.fn() } as unknown as Response
    const mockNext = jest.fn()

    await processRequest(mockRequest, mockResponse, mockNext)

    expect(mockNext).toHaveBeenCalledWith(testError)
    expect(mockResponse.status).not.toHaveBeenCalled()
  })
})

核心要点:

  • 用jest.mock直接mock整个模块,返回Jest mock函数
  • 用mockResolvedValue/mockRejectedValue快速设置异步返回值
  • 利用Jest内置的断言方法(toHaveBeenCalledWith等)简化验证逻辑

关键注意事项

  • 禁止手动创建不调用resolve/reject的Promise作为Stub返回值,这是测试超时的常见诱因
  • 测试后必须恢复原始模块或清除Mock,避免不同测试用例间的状态污染
  • 同时覆盖成功、异常分支,确保代码逻辑的完整性验证

内容的提问来源于stack exchange,提问作者Abraham P

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 19:05:23