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

如何在不暴露内部函数及不用超时的情况下测试JS异步函数结果?

异步邮件发送测试问题解决方案

问题背景

现有一段模拟账户创建的代码,createAccount函数会将账户信息存入数组,随后异步调用sendEmail模拟发送通知邮件,但不会等待邮件发送完成。编写测试用例验证邮件发送时,因sendEmail的异步特性,断言执行时邮件还未发送完成,导致测试失败。

原账户创建代码

export const accounts = [];
export const emails = [];

export async function createAccount(name, email) {
    accounts.push({name, email});
    void sendEmail(email);
}

async function sendEmail(email) {
    setTimeout(0, () => emails.push(`Account created to ${email}`));
}

原测试代码

import {assert} from 'chai';
import {createAccount, emails} from './index.js';

describe('test', async () => {
  it('test', async() => {
    await createAccount('John Doe', 'test@example.com');
    assert(emails[0], 'Account created to test@example.com');
  });
});

测试报错信息

$ npm run test

> foo@1.0.0 test
> mocha test.js



  test
    1) test


  0 passing (5ms)
  1 failing

  1) test
       test:
     AssertionError: Account created to test@example.com
      at Context.<anonymous> (file:///home/me/sandbox/foo/test.js:7:5)

问题原因

sendEmail内部使用setTimeout触发异步操作,属于宏任务。测试中await createAccount仅等待createAccount函数本身完成,不会等待sendEmail里的setTimeout回调执行,因此断言时emails数组还未被填充。

要求

  • 不暴露sendEmail的返回值;
  • 不使用睡眠/超时机制。

解决方案

方案1:Mock全局setTimeout同步执行回调

通过替换全局的setTimeout,让其立即执行传入的回调函数,消除异步延迟,确保测试时emails数组能被同步填充。

修改后的测试代码:

import { assert } from 'chai';
import { createAccount, emails } from './index.js';

describe('账户创建邮件测试', () => {
  let originalSetTimeout;

  beforeEach(() => {
    // 保存原始setTimeout
    originalSetTimeout = global.setTimeout;
    // Mock setTimeout为同步执行
    global.setTimeout = (callback) => {
      callback();
      return 0;
    };
    // 清空emails数组,避免测试污染
    emails.length = 0;
  });

  afterEach(() => {
    // 恢复原始setTimeout
    global.setTimeout = originalSetTimeout;
  });

  it('应成功发送账户创建通知邮件', async () => {
    await createAccount('John Doe', 'test@example.com');
    assert.equal(emails[0], 'Account created to test@example.com');
  });
});

方案2:利用Node.js事件循环机制等待宏任务执行

通过setImmediate封装Promise,等待当前事件循环中的所有宏任务(包括setTimeout的回调)执行完毕后再进行断言。

修改后的测试代码:

import { assert } from 'chai';
import { createAccount, emails } from './index.js';

describe('账户创建邮件测试', () => {
  beforeEach(() => {
    emails.length = 0;
  });

  it('应成功发送账户创建通知邮件', async () => {
    await createAccount('John Doe', 'test@example.com');
    // 等待当前宏任务队列执行完成
    await new Promise(resolve => setImmediate(resolve));
    assert.equal(emails[0], 'Account created to test@example.com');
  });
});

附录:测试环境配置(package.json)

{
  "name": "foo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "mocha test.js"
  },
  "type": "module",
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "chai": "^5.1.1",
    "mocha": "^10.6.0"
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 08:17:36