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

为何我的Jest测试中URL.createObjectURL调用断言失败?

单元测试失败排查:第二个downloadPDFFile测试未触发createObjectURL调用

我写了下面两个单元测试:

test('should call downloadPDFFile with msSaveOrOpenBlob when the navigator is present', async () => {
  axios.get.mockResolvedValueOnce({ data: 'blob' });
  window.navigator.msSaveOrOpenBlob = () => true;
  const msSaveOrOpenBlobSpy = jest.spyOn(window.navigator, 'msSaveOrOpenBlob');
  await downloadPDFFile('www.pokebook/{+asset_path}', 'squirtle', '123', false);
  expect(msSaveOrOpenBlobSpy).toHaveBeenCalledWith('blob', 'squirtle.pdf');
});

test('should call downloadPDFFile with createObjectURL when the navigator is not present', async () => {
  axios.get.mockResolvedValueOnce({ data: 'blob' });
  window.URL.createObjectURL = () => 'www.pokeball.com';
  console.log(window.navigator);
  const urlCreateObjectSpy = jest.spyOn(window.URL, 'createObjectURL');
  await downloadPDFFile('www.pokebook/{+asset_path}', 'charmander', '123', false);
  expect(urlCreateObjectSpy).toHaveBeenCalledWith('blob');
});

查阅资料后尝试设置window.URL.createObjectURL = () => {};,但第二个测试始终失败,报错如下:

expect(jest.fn()).toHaveBeenCalledWith(...expected)

    Expected: "blob"

    Number of calls: 0

      557 |             const urlCreateObjectSpy = jest.spyOn(window.URL, 'createObjectURL');
      558 |             await downloadPDFFile('www.pokebook/{+asset_path}', 'charmander', '123', false);
    > 559 |             expect(urlCreateObjectSpy).toHaveBeenCalledWith('blob');
          |                                        ^
      560 |         });
      561 |     });
      562 | });

被测试的函数代码:

const downloadPDFFile = (urlTemplate, name, readToken, isWatermarked) => {
  const config = { responseType: "blob" };
  const pdfFileName = `${name.split(".")[0]}.pdf`;
  let formattedUrl;

  if (isWatermarked) {
    formattedUrl = urlTemplate.replace("{+temp}?", `?token=${readToken}&`);
  } else {
    formattedUrl = urlTemplate.replace("{+temp}", `?token=${readToken}`);
  }

  axios.get(formattedUrl, config).then((response) => {
    if (window.navigator && window.navigator.msSaveOrOpenBlob) {
      window.navigator.msSaveOrOpenBlob(response.data, pdfFileName);
    } else {
      const anchor = document.createElement("a");
      anchor.download = pdfFileName;
      anchor.href = URL.createObjectURL(response.data);
      document.body.appendChild(anchor);
      anchor.click();
      document.body.removeChild(anchor);
    }
  });
};

我怀疑是测试之间遗漏了清理步骤——第二个测试里console.log显示window.navigator对象还保留着之前的属性,这是不是导致测试失败的原因?


问题原因及解决方案

1. 测试间未清理全局变量

第一个测试给window.navigator添加了msSaveOrOpenBlob属性,Jest默认不会在测试间重置全局对象的自定义属性,所以第二个测试中window.navigator.msSaveOrOpenBlob依然存在,代码直接走了IE兼容分支,根本没进入调用createObjectURL的逻辑。

解决方法:
添加afterEach钩子,在每个测试结束后清理全局属性并重置mock:

afterEach(() => {
  // 清除添加的msSaveOrOpenBlob属性
  if (window.navigator.msSaveOrOpenBlob) {
    delete window.navigator.msSaveOrOpenBlob;
  }
  // 重置所有Jest spy
  jest.restoreAllMocks();
});

2. downloadPDFFile未返回Promise,await无效

你的downloadPDFFile函数没有返回axios.get的Promise,测试里的await根本没等异步请求的回调执行完成,断言会提前触发,自然捕获不到createObjectURL的调用。

解决方法:
修改函数,返回axios请求的Promise:

const downloadPDFFile = (urlTemplate, name, readToken, isWatermarked) => {
  const config = { responseType: "blob" };
  const pdfFileName = `${name.split(".")[0]}.pdf`;
  let formattedUrl;

  if (isWatermarked) {
    formattedUrl = urlTemplate.replace("{+temp}?", `?token=${readToken}&`);
  } else {
    formattedUrl = urlTemplate.replace("{+temp}", `?token=${readToken}`);
  }

  // 返回axios.get的Promise,让await能等待异步操作完成
  return axios.get(formattedUrl, config).then((response) => {
    if (window.navigator && window.navigator.msSaveOrOpenBlob) {
      window.navigator.msSaveOrOpenBlob(response.data, pdfFileName);
    } else {
      const anchor = document.createElement("a");
      anchor.download = pdfFileName;
      anchor.href = URL.createObjectURL(response.data);
      document.body.appendChild(anchor);
      anchor.click();
      document.body.removeChild(anchor);
    }
  });
};

3. 第二个测试显式确保msSaveOrOpenBlob不存在

可以在第二个测试里主动清除该属性,避免残留影响:

test('should call downloadPDFFile with createObjectURL when the navigator is not present', async () => {
  axios.get.mockResolvedValueOnce({ data: 'blob' });
  // 显式删除属性,确保进入非IE分支
  delete window.navigator.msSaveOrOpenBlob;
  window.URL.createObjectURL = () => 'www.pokeball.com';
  const urlCreateObjectSpy = jest.spyOn(window.URL, 'createObjectURL');
  await downloadPDFFile('www.pokebook/{+asset_path}', 'charmander', '123', false);
  expect(urlCreateObjectSpy).toHaveBeenCalledWith('blob');
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 09:15:38