js单元测试中mock性能接口的核心是可控、可断言、可隔离地验证采集与响应逻辑,而非模拟真实耗时;应按需mock实际调用的方法,用状态机控制多次返回值,抽离时间采集为可注入依赖,并避免滥用fake timers。

在 JS 单元测试中 Mock 性能时间接口,核心目标不是“模拟真实耗时”,而是**可控、可断言、可隔离地验证代码对性能指标的采集、判断或响应逻辑**。比如你调用 performance.now()、performance.mark()、performance.measure() 或封装的监控 SDK,测试重点应落在“它是否在正确时机记录了标记”“是否计算了预期差值”“是否触发了超时回调”等行为上,而非让测试变慢。
只 Mock 实际调用的性能 API 方法
真实 performance 对象有十多个方法,但你的业务代码很可能只用其中 2~3 个。Mock 时不要照搬整个对象,而是按测试用例反推:
- 检查测试中实际调用了哪些方法(如
performance.now()、performance.mark('api-start')) - 提取它们的调用频次、参数特征、返回值类型(例如
now()返回 number,mark()无返回) - 用 plain object 或 jest.fn() 构建最小可用替身,其余方法一概不定义(避免意外调用报错)
示例:
const mockPerformance = {now: jest.fn(() => 1000),
mark: jest.fn(),
measure: jest.fn(),
// 不实现 getEntries、clearMarks 等未使用的函数
};
global.performance = mockPerformance;
用状态机控制多次调用的返回值
当测试涉及“起始时间 → 某操作 → 结束时间 → 计算耗时”这类链路时,不能让所有 now() 都返回同一数值。改用闭包变量记录调用顺序,动态返回递增值:
const mockPerformance = {
now: () => {
nowCalls++;
return nowCalls === 1 ? 1000 : 1050; // 第一次1000ms,第二次1050ms
},
mark: jest.fn(),
reset: () => { nowCalls = 0; }
};
global.performance = mockPerformance;
测试结束后调用 mockPerformance.reset(),确保不影响其他用例。
把时间采集逻辑抽离为可注入依赖
真正难测的是把 performance.now() 直接写死在业务函数里。更健壮的做法是将时间采集行为抽象成一个接口(如 getTimeStamp()),再通过参数或依赖注入传入:
function trackApiLatency(getTime = () => performance.now()) {
const start = getTime();
return fetch('/api').then(() => {
const end = getTime();
console.log(`耗时: ${end - start}ms`);
});
}
// 测试中传入可控的时间生成器
test('记录API耗时', () => {
const mockTime = [1000, 1080];
let idx = 0;
const getTime = () => mockTime[idx++];
trackApiLatency(getTime);
expect(console.log).toHaveBeenCalledWith('耗时: 80ms');
});
这种方式无需篡改全局 performance,也更容易复用和组合不同场景(如模拟超时、抖动、负值等边界情况)。
避免 Mock 底层计时器(如 setTimeout)除非必要
如果测试目标是“某段逻辑是否在 100ms 内完成”,不要用 jest.useFakeTimers() 去控制 setTimeout;那是在测执行延迟,不是测性能采集逻辑。只有当你明确要验证“超时降级逻辑是否触发”时,才需配合 jest.advanceTimersByTime(110) 等操作。多数性能监控单元测试,只需 Mock 时间戳本身即可。











