使用Jest测试事件发射器遇超时错误:Async回调未在超时内触发
解决Jest测试S3 CSV解析异步函数超时问题
问题场景
我编写了一个从AWS S3下载CSV文件并解析的异步函数downloadCsvFile,使用Jest编写测试用例时,S3库的Mock已经正常工作,但事件监听器on触发了超时错误。
原函数代码
import { S3 } from 'aws-sdk'; import * as csvParse from 'fast-csv'; async function downloadCsvFile(fileName: string, bucketName: string): Promise<any> { let parseStreamData; var params = { Bucket: bucketName, Key: fileName, }; try { await new S3().headObject(params).promise(); let csvReadStream = new S3().getObject(params).createReadStream(); let csvFileArray: Array<any> = []; parseStreamData = new Promise((resolve, reject) => { csvParse .parseStream(csvReadStream, { headers: true }) .on('data', function(data) { csvFileArray.push(data); }) .on('end', function() { resolve(csvFileArray); }) .on('error', function() { reject('csv parse process failed'); }); }); } catch (error) { console.log(error); return []; } return await parseStreamData; }
原测试代码
import { bucketHandler } from './bucketHandler'; import { mocked } from 'ts-jest/utils'; import { parseStream } from "fast-csv"; import { S3 } from 'aws-sdk'; jest.mock('aws-sdk'); jest.mock("fast-csv"); describe('handler', () => { test('downloadCsvFile', async () => { mocked(S3).mockImplementationOnce((): any => ({ headObject: () => ({ promise: () => Promise.resolve(), }), })); mocked(S3).mockImplementationOnce((): any => { return { getObject: () => { return { createReadStream: () => { return {} }, }; }, }; }); mocked(parseStream).mockImplementationOnce((): any => ({ on: () => { return { on: () => { return { on: () => { return {} } } } } }, })); expect(await bucketHandler.downloadCsvFile('1', '1')).toEqual([{}]); }); });
测试错误信息
$ npm run test src/aws-s3/bucketHandler.test.ts FAIL src/aws-s3/bucketHandler.test.ts (6.221s) handler × downloadCsvFile (5015ms) ● handler › downloadCsvFile : Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Error: 8 | 9 | describe('handler', () => { > 10 | test('downloadCsvFile', async () => { | ^ 11 | mocked(S3).mockImplementationOnce((): any => ({ 12 | headObject: () => ({ 13 | promise: () => Promise.resolve(), at new Spec (node_modules/jest-jasmine2/build/jasmine/Spec.js:116:22) at Suite.<anonymous> (src/aws-s3/bucketHandler.test.ts:10:5) at Object.<anonymous> (src/aws-s3/bucketHandler.test.ts:9:1) Test Suites: 1 failed, 1 total Tests: 1 failed, 1 total Snapshots: 0 total Time: 6.77s, estimated 13s Ran all test suites matching /src\\\\aws-s3\\\\bucketHandler.test.ts/i.
问题原因
当前的parseStream Mock只是链式返回空对象,没有触发end事件对应的回调函数,导致函数中创建的parseStreamData Promise一直处于pending状态,Jest等待超时。
解决方案
修改parseStream的Mock逻辑,保存事件回调并手动触发,让Promise正常resolve:
修改后的测试代码
import { bucketHandler } from './bucketHandler'; import { mocked } from 'ts-jest/utils'; import * as csvParse from "fast-csv"; import { S3 } from 'aws-sdk'; jest.mock('aws-sdk'); jest.mock("fast-csv"); describe('handler', () => { test('downloadCsvFile', async () => { // Mock S3 headObject 校验文件存在 mocked(S3).mockImplementationOnce((): any => ({ headObject: () => ({ promise: () => Promise.resolve(), }), })); // Mock S3 getObject 返回空流对象 mocked(S3).mockImplementationOnce((): any => ({ getObject: () => ({ createReadStream: () => ({}), }), })); // 存储data和end事件的回调函数 let dataCallback: (data: any) => void; let endCallback: () => void; // Mock parseStream的on方法,根据事件类型保存回调 mocked(csvParse.parseStream).mockImplementationOnce(() => ({ on: (event: string, callback: Function) => { if (event === 'data') { dataCallback = callback as (data: any) => void; } else if (event === 'end') { endCallback = callback as () => void; } // 保持链式调用的返回值 return this; }, })); // 启动测试函数,此时Promise还未resolve const testPromise = bucketHandler.downloadCsvFile('1', '1'); // 触发data事件,模拟解析到一条CSV数据 dataCallback({ id: 1, name: 'test_data' }); // 触发end事件,让Promise完成resolve endCallback(); // 验证返回结果 expect(await testPromise).toEqual([{ id: 1, name: 'test_data' }]); }); });
关键修改点
- 导入匹配:将
parseStream的导入改为和原函数一致的* as csvParse,确保Mock的是同一个模块导出的方法 - 保存回调:在Mock
on方法时,把data和end事件的回调函数保存下来 - 手动触发事件:启动测试函数后,主动调用保存的回调函数,触发
data添加测试数据,再触发end让Promise正常resolve
这样就能解决超时问题,因为Promise会在end事件触发后完成,Jest能在超时前拿到结果。
内容的提问来源于stack exchange,提问作者rey patoy
相关产品推荐
相关产品推荐

