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

如何使用Jest对轮询WebWorker代码进行单元测试?

好问题!针对这个轮询WebWorker的Jest测试,我们可以通过重构代码提升可测性+Mock环境与依赖的方式,覆盖你关心的所有场景,一步步来:

第一步:重构Worker代码,暴露可测试的逻辑

原代码里的onmessage逻辑和fetchLatest都是内部函数,不利于测试。我们可以把核心逻辑抽成可导出的函数,同时保留原有的Worker行为:

// worker.ts
import { fetchLatestResults } from '../backend/fetchLatestResults';

let polling = false;
let symbolIds: string[] = [];

// 导出fetchLatest,方便测试单独调用
export const fetchLatest = async () => {
  if (!symbolIds.length) {
    return;
  }
  try {
    const results = await fetchLatestResults(symbolIds);
    self.postMessage(results);
  } catch (e) {
    // noop for now
  }
};

// 把onmessage的逻辑抽成独立函数并导出
export const handleMessage = (e: MessageEvent) => {
  symbolIds = e.data;
  fetchLatest();
};

// 初始化轮询逻辑保持不变
if (!polling) {
  polling = true;
  fetchLatest();
  setInterval(fetchLatest, 10 * 1000);
}

self.onmessage = handleMessage;
第二步:Mock WebWorker环境与依赖

Jest运行在Node.js环境中,没有浏览器的self(Worker全局对象),也不会真的执行定时器。我们需要模拟这些环境,同时mock业务依赖fetchLatestResults:

// worker.test.ts
import { fetchLatest, handleMessage } from './worker';
import { fetchLatestResults } from '../backend/fetchLatestResults';

// Mock业务依赖,避免真实请求
jest.mock('../backend/fetchLatestResults');

// 模拟Worker的全局self对象,重点mock postMessage
global.self = {
  postMessage: jest.fn(),
} as unknown as WorkerGlobalScope;

// 使用Jest的假定时器,避免测试中等待真实的10秒
jest.useFakeTimers();

// 每个测试后清理mock和定时器,防止污染
afterEach(() => {
  jest.clearAllMocks();
  jest.clearAllTimers();
});
第三步:编写核心场景测试用例

现在可以针对你关心的场景逐一编写测试:

场景1:symbolIds为空时,不触发请求

test('当symbolIds为空数组时,fetchLatest不会调用fetchLatestResults', async () => {
  // 通过handleMessage设置空的symbolIds
  handleMessage({ data: [] } as MessageEvent);
  
  await fetchLatest();
  
  // 验证依赖未被调用,也没有发送消息
  expect(fetchLatestResults).not.toHaveBeenCalled();
  expect(self.postMessage).not.toHaveBeenCalled();
});

场景2:symbolIds有值时,正常请求并发送结果

test('当symbolIds有值时,fetchLatest会请求数据并post结果', async () => {
  // 给mock函数设置返回值
  const mockResponse = [{ symbolId: 'BTC', price: 30000 }];
  (fetchLatestResults as jest.Mock).mockResolvedValue(mockResponse);
  
  // 设置有效的symbolIds
  handleMessage({ data: ['BTC'] } as MessageEvent);
  
  await fetchLatest();
  
  // 验证依赖调用正确,且postMessage发送了预期结果
  expect(fetchLatestResults).toHaveBeenCalledWith(['BTC']);
  expect(self.postMessage).toHaveBeenCalledWith(mockResponse);
});

场景3:接收消息时,更新symbolIds并立即触发请求

test('收到message时,会更新symbolIds并立刻执行一次fetchLatest', async () => {
  const mockResponse = [{ symbolId: 'ETH', price: 1800 }];
  (fetchLatestResults as jest.Mock).mockResolvedValue(mockResponse);
  
  // 模拟Worker收到消息
  handleMessage({ data: ['ETH'] } as MessageEvent);
  
  // 等待异步的fetchLatest执行完成
  await Promise.resolve();
  
  expect(fetchLatestResults).toHaveBeenCalledWith(['ETH']);
  expect(self.postMessage).toHaveBeenCalledWith(mockResponse);
});

场景4:轮询逻辑正确初始化并定时执行

test('Worker初始化时会启动轮询,每10秒调用一次fetchLatest', async () => {
  // 重新导入Worker模块,触发初始化逻辑(用resetModules避免缓存影响)
  jest.resetModules();
  global.self = { postMessage: jest.fn() } as unknown as WorkerGlobalScope;
  const { fetchLatest } = require('./worker');
  
  // 验证初始化时调用了一次fetchLatest
  expect(fetchLatest).toHaveBeenCalledTimes(1);
  
  // 快进10秒,模拟定时器触发
  jest.advanceTimersByTime(10 * 1000);
  await Promise.resolve();
  
  // 验证轮询触发了第二次调用
  expect(fetchLatest).toHaveBeenCalledTimes(2);
});
额外提示
  • 如果模块级的polling或symbolIds在多次测试中保持状态,可以在beforeEach里用jest.resetModules()重置模块,确保每次测试都是干净的环境。
  • 对于错误场景(比如fetchLatestResults抛出异常),可以给mock函数设置mockRejectedValue,验证代码是否正确捕获了异常(虽然你的代码里是noop,但可以测试是否没有抛出未捕获的错误)。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.07 11:32:27