如何使用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
相关产品推荐
相关产品推荐

