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
相关产品推荐
相关产品推荐

