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

Sinon stub ffmpeg error事件监听器时upload.abort未触发问题

ffmpeg转码上传S3单元测试error事件stub失效问题

问题复现

业务代码如下:

const {writeStream, upload} = S3Service.uploadStream({ Bucket: process.env.BUCKET, Key: s3Path});

ffmpeg(stream)
    .outputOptions('-copyts')
    .audioCodec("libopus")
    .toFormat("matroska")
    .on('error', (err, stdout, stderr) => {
        if (err) {
            console.log(err.message);
            upload.abort();
            return reject("Error FFMPEG");
        }
    })
    .on('start', (p) => console.log(p))
    .on(`end`, () => console.log("end ffmpeg"))
    .pipe(writeStream);

upload.promise()
    .then(() => resolve("Successful audio converted transfer"))
    .catch((err) => console.error(err));

初始测试stub逻辑:

let uploadStreamStub = {
    writeStream: sandbox.stub().returnsThis(),
    upload: {
        promise: sandbox.stub(),
        abort: sandbox.stub()
     }
}

sandbox.stub(s3Service, "uploadStream").returns(uploadStreamStub);

尝试stub error事件验证upload.abort()调用的测试代码:

let onStub = sandbox.stub(ffmpeg.prototype, "on").returnsThis();
onStub.withArgs("error").yieldsAsync(new Error("test"));
sandbox.assert.calledOnce(uploadStreamStub.upload.abort);

执行后抛出错误:

AssertError: expected stub to be called once but was called 0 times

注释掉error事件触发逻辑后,验证upload.promise调用的测试可以正常通过,仅on('error')相关逻辑不生效。


问题根因

写法存在两个核心问题:

  • 异步时序错误
    yieldsAsync是异步触发回调,会把回调逻辑放到事件循环的下一个tick执行,但断言逻辑是同步执行的,断言跑的时候error回调还没执行,abort()根本没被调用,自然计数为0。
  • stub覆盖范围不全
    仅stub了ffmpeg.prototype.on方法,但ffmpeg是链式调用,on执行完后还会依次调用outputOptions/audioCodec/toFormat/pipe等方法,这些方法没有被stub的话,链式调用会提前抛错,根本走不到error回调注册、触发的逻辑。另外仅配置error事件的on匹配,start/end事件的on调用也可能因为返回值异常打断链式执行。

修复方案

不要直接stub ffmpeg原型,改为构造完整的链式调用mock对象,手动控制error回调的触发时机,同时用async/await保证断言在异步逻辑执行完成后再运行:

it('should trigger upload.abort when ffmpeg throws error', async () => {
  // 构造S3上传mock
  const uploadMock = {
    promise: sandbox.stub().resolves(),
    abort: sandbox.stub()
  }
  const writeStreamMock = {}
  sandbox.stub(s3Service, 'uploadStream').returns({
    writeStream: writeStreamMock,
    upload: uploadMock
  })

  // 存下业务代码注册的error回调
  let errorHandler = null
  // 构造完整的ffmpeg链式调用mock
  const ffmpegMock = {
    outputOptions: sandbox.stub().returnsThis(),
    audioCodec: sandbox.stub().returnsThis(),
    toFormat: sandbox.stub().returnsThis(),
    on: sandbox.stub().callsFake((event, cb) => {
      if (event === 'error') errorHandler = cb
      return ffmpegMock
    }),
    pipe: sandbox.stub().returnsThis()
  }
  // stub ffmpeg工厂函数(fluent-ffmpeg不需要new调用,直接stub模块导出即可)
  sandbox.stub(ffmpeg).returns(ffmpegMock)

  // 执行业务逻辑
  const task = runFfmpegUpload() // 替换为你实际包裹这段业务逻辑的Promise方法

  // 手动触发error回调
  errorHandler(new Error('test ffmpeg error'), '', '')

  // 等业务逻辑执行完成再断言
  await expect(task).to.be.rejectedWith('Error FFMPEG')
  sandbox.assert.calledOnce(uploadMock.abort)
})

关键改动说明

  • 直接stub ffmpeg模块的导出(工厂函数),返回构造好的链式mock对象,所有链式方法都配置returnsThis(),避免链式调用提前抛错
  • 用callsFake接管on方法逻辑,主动保存业务代码注册的error回调,不需要依赖yieldsAsync,触发时序完全可控
  • 测试用例改为async/await写法,等业务Promise状态落定后再执行断言,解决异步时序问题

内容的提问来源于stack exchange,提问作者Pedro Elias

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 19:12:31