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

Sinon clock.tick无法推进setTimeout时间的测试问题

这个问题我之前也碰到过,核心是没处理好async函数的Promise和sinon假定时器的微任务执行顺序。咱们一步步来解决:

问题根因分析

你写的foo是async函数,调用它会立即返回一个Promise,但函数内部的await setTimeout逻辑是异步的。原来的测试里犯了两个关键错误:

  1. 没有等待foo返回的Promise完成,导致在clock.tick后,Promise的resolve回调触发了,但await后面的f()还在微任务队列里没执行,这时候断言自然失败。
  2. 没必要tick 100000毫秒,60000毫秒就够触发那个setTimeout了。

修正后的测试代码

把测试函数改成async,并且等待foo的Promise完成,同时给测试加上时钟的前后置处理避免污染其他用例:

const sinon = require('sinon');
const expect = require('chai').expect;
const { foo } = require('./foo');

describe('Module Foo ', function() {
  let clock;

  // 每个测试前初始化假时钟
  beforeEach(function() {
    clock = sinon.useFakeTimers();
  });

  // 每个测试后恢复真实时钟
  afterEach(function() {
    clock.restore();
  });

  it('calls function after 1 minute', async function() {
    const bar = sinon.stub();
    const fooPromise = foo(bar); // 保存async函数返回的Promise

    // 初始状态下bar未被调用
    expect(bar.called).to.be.false;

    // 快进60秒,触发setTimeout的resolve
    clock.tick(60000);

    // 等待foo内部的所有异步逻辑执行完毕
    await fooPromise;

    // 此时bar已经被调用
    expect(bar.called).to.be.true;
  });
});

另一种简化写法(不需要保存Promise)

如果不想额外保存fooPromise,也可以在clock.tick后等待一个空Promise,触发微任务队列执行:

it('calls function after 1 minute', async function() {
  const bar = sinon.stub();
  foo(bar);

  expect(bar.called).to.be.false;
  clock.tick(60000);
  
  // 等待微任务队列处理完(也就是await后面的f()执行)
  await Promise.resolve();

  expect(bar.called).to.be.true;
});

为什么这样能解决问题?

当你调用clock.tick(60000)时,sinon会触发fake setTimeout的回调,resolve那个等待的Promise。但await Promise.resolve()之后的代码(也就是f())会被放到微任务队列里,Node.js的事件循环会先处理完当前宏任务,再处理微任务。所以我们需要用await来等待微任务队列执行完毕,才能确保bar已经被调用。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.12 05:27:26