【问题标题】:Jest can't test an awaited promise, it times out insteadJest 无法测试等待的承诺,而是超时
【发布时间】:2021-11-18 21:14:22
【问题描述】:

我从只运行 axios GET 切换到返回一个承诺,现在我的 Jest 测试失败了:

下载 'resource.js' 中的 zip:

async function downloadMtgJsonZip() {
  const path = Path.resolve(__dirname, 'resources', fileName);
  const writer = Fs.createWriteStream(path);

  console.info('...connecting...');
  const { data, headers } = await axios({
    url,
    method: 'GET',
    responseType: 'stream',
  });
  return new Promise((resolve, reject) => {
    let error = null;
    const totalLength = headers['content-length'];
    const progressBar = getProgressBar(totalLength);
    console.info('...starting download...');
    data.on('data', (chunk) => progressBar.tick(chunk.length));
    data.pipe(writer);
    writer.on('error', (err) => {
      error = err;
      writer.close();
      reject(err);
    });
    writer.on('close', () => {
      const now = new Date();
      console.info(`Completed in ${(now.getTime() - progressBar.start) / 1000} seconds`);
      if (!error) resolve(true);
      // no need to call the reject here, as it will have been called in the
      // 'error' stream;
    });
  });
}

'resource.spec.js' 中的以下测试均未通过:

it('fetches successfully data from an URL', async () => {
    const onFn = jest.fn();
    const data = { status: 200, data: { pipe: () => 'data', on: onFn }, headers: { 'content-length': 100 } };

    const writerOnFn = jest.fn();

    axios.mockImplementationOnce(() => data);
    fs.createWriteStream.mockImplementationOnce(() => ({ on: writerOnFn }));
    await downloadMtgJsonZip();
    expect(onFn).toHaveBeenCalledWith('data', expect.any(Function));
    expect(axios).toHaveBeenCalledWith(
      expect.objectContaining({ url: 'https://mtgjson.com/api/v5/AllPrintings.json.zip' }),
    );
    expect(axios).toHaveBeenCalledWith(
      expect.objectContaining({ responseType: 'stream' }),
    );
  });
  it('ticks up the progress bar', async () => {
    const tickFn = jest.fn();
    const dataOnFn = jest.fn((name, func) => func(['chunk']));
    const data = { status: 200, data: { pipe: () => 'data', on: dataOnFn }, headers: { 'content-length': 1 } };

    const writerOnFn = jest.fn();

    ProgressBar.mockImplementationOnce(() => ({ tick: tickFn }));
    axios.mockImplementationOnce(() => data);
    fs.createWriteStream.mockImplementationOnce(() => ({ on: writerOnFn }));
    await downloadMtgJsonZip();

    expect(ProgressBar).toHaveBeenCalledWith(
      expect.stringContaining('downloading'),
      expect.objectContaining({
        total: 1,
      }),
    );
    expect(tickFn).toHaveBeenCalledWith(1);
  });
});

值得注意的是,VSCode 告诉我,对于“resource.js”中的 axios,“此表达式不可调用”并且 nothing 具有 mockImplementationOnce(它“不存在于类型...')。

以前我的downloadMtgJsonZip 是这样的:

async function downloadMtgJsonZip() {
  const path = Path.resolve(__dirname, 'resources', 'AllPrintings.json.zip');
  const writer = Fs.createWriteStream(path);

  console.info('...connecting...');
  const { data, headers } = await axios({
    url,
    method: 'GET',
    responseType: 'stream',
  });
  const totalLength = headers['content-length'];
  const progressBar = getProgressBar(totalLength);
  const timer = setInterval(() => {
    if (progressBar.complete) {
      const now = new Date();
      console.info(`Completed in ${(now.getTime() - progressBar.start) / 1000} seconds`);
      clearInterval(timer);
    }
  }, 100);
  console.info('...starting download...');
  data.on('data', (chunk) => progressBar.tick(chunk.length));
  data.pipe(writer);
}

测试中唯一不同的是 createWriteStream 的模拟更简单(它读取fs.createWriteStream.mockImplementationOnce(() => 'fs');

我已经尝试添加:

  afterEach(() => { 
    jest.clearAllMocks(); 
    jest.resetAllMocks();
  });

我尝试添加writerOnFn('close'); 以尝试让writer.on('close', ...) 触发。

但我直到得到这个错误:

:超时 - 在 jest.setTimeout.Timeout 指定的 5000 毫秒超时内未调用异步回调。在 jest.setTimeout.Error 指定的 5000 毫秒超时内未调用异步回调:

我无法弄清楚缺少什么来使异步调用被“调用”。 last time I had this issue 模拟出 createWriteStream 解决了我的问题,但我没有看到其他可以模拟的内容?

如何让这些测试再次通过?

【问题讨论】:

  • 如果您使用jest.setTimeout(30000) 增加测试超时,它们会起作用吗?
  • @Mackan90096 当我在家中访问代码时,我会尝试这样做。是否有部分代码或测试代码会超过默认超时?
  • 据我所知,唯一会超过超时的部分是您正在阅读的文件非常大。
  • @Mackan90096 axios 调用和 createWriteStream 都被模拟了?他们不应该接触任何真实的文件
  • 我的错,我看错了代码。我现在唯一能想到的是,模拟的fs.createWriteStream 没有调用您正在查看的on 函数。测试像 fs.createWriteStream 这样的 EventEmitters 的(最佳)解决方案是使用 callback in jest

标签: javascript node.js testing axios jestjs


【解决方案1】:

Jest 的异步测试完成默认超时时间为 5000 毫秒(参考:https://jestjs.io/docs/configuration#testtimeout-number

如果您有长时间运行的异步调用,则有必要提高此阈值。

例如在我的jest.config.js 中,超时设置为 60000 毫秒

module.exports = {
  ...
  testTimeout: 60000, 
}

【讨论】:

  • 我的异步调用都应该被模拟出来,而且一点也不花时间,除非你能在我的代码中发现一个没有被模拟出来的调用?
  • 这个我试过了,还是不行
【解决方案2】:

使用writer.on(event, handler) 附加的事件处理程序如何在测试代码中被调用? writerOnFn mock 不需要调用传入的处理函数吗?如果这些没有被调用,那么 resolve(true) 将永远不会被调用,因此在测试中对 await downloadMtgJsonZip(); 的调用永远不会解析。

我认为你需要这样的东西

const writerOnFn = jest.fn((e, cb) => if (e === 'close') cb())

当然,您可能想要充实它以区分“错误”和“关闭”事件,或者如果您有围绕“错误”条件的测试,请确保更改它。

【讨论】:

  • 这看起来很有希望!自从发布问题以来,我一直试图只打电话给writerOnFn('close'),但我没想过要做你在这里写的事情。我会尽快尝试(现在,希望在早上)
  • 成功了!太棒了
  • 很高兴为您提供帮助
猜你喜欢
  • 2017-09-14
  • 2020-07-19
  • 1970-01-01
  • 2021-10-10
  • 2016-07-23
  • 2020-06-06
  • 1970-01-01
  • 1970-01-01
  • 2013-09-16
相关资源
最近更新 更多