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

使用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' }]);
    });
});

关键修改点

  1. 导入匹配:将parseStream的导入改为和原函数一致的* as csvParse,确保Mock的是同一个模块导出的方法
  2. 保存回调:在Mockon方法时,把data和end事件的回调函数保存下来
  3. 手动触发事件:启动测试函数后,主动调用保存的回调函数,触发data添加测试数据,再触发end让Promise正常resolve

这样就能解决超时问题,因为Promise会在end事件触发后完成,Jest能在超时前拿到结果。

内容的提问来源于stack exchange,提问作者rey patoy

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 19:50:21