首页 > 解决方案 > 使用 Jest 在递归函数中模拟 Promise 和 Timers

问题描述

我试图在单元测试中覆盖一些代码(特别是 catch 块内的代码),我认为它失败是由于内存泄漏错误,可能是由于没有正确模拟计时器。

以下是主要代码的示例:

    const delay = new Promise(resolve => setTimeout(() => resolve(), 1000);

    const readCustomerStatus = async (retry = 2) => {

    let content = '';
    try{
      await delay;
      content = await fs.readFileSync(CUSTOMER_PATH,'utf8');
    }
    catch(error){
      while(retry > 0){
        readCustomerStatus(retry - 1);
      }
    }
    return content;
    }

这是单元测试代码:

    test('Should return error when readFileSync fails'),async()=>{

    fs.readFileSync = jest.fn()
    .mockRejectedValueOnce(new Error('readFileSync failed'))
    .mockRejectedValueOnce(new Error('readFileSync failed'));
    const customerStatusResult = await readCustomerStatus();
      expect(customerStatusResult).toThrow(‘readFileSync failed’);

    }

当我从主代码中删除递归调用时,单元测试工作。看起来延迟承诺可能会以某种方式影响测试。我也尝试在 beforeAll 函数中添加 jest.useFakeTimers() ,但这似乎也没有帮助。

标签: javascriptunit-testingjestjs

解决方案


看起来您的代码有多个问题,其中大多数与您(不)等待 Promises 的方式有关。

当您以Promise这种方式创建时,它将立即开始执行,并且只会解决一次:

// Timer starts right away, which I believe it is not what you wanted
const delay = new Promise(resolve => setTimeout(() => resolve(), 1000);

所以这是行为:

   await delay // Awaits `delay` Promise to be resolved.
               // Depending on when it started, might be less than 1 second
   await delay // This second invocation will be already resolved. 0 seconds delay.

您的递归版本也是如此。delay您正在多次等待相同的Promise(每次递归),因此您实际上并没有在每次迭代中暂停 1 秒。

在这里,您还有另一个问题:

    catch(error){
      while(retry > 0){
        readCustomerStatus(retry - 1); // issue: readCustomerStatus is async
      }
    }

在这一行中,您正在调用一个没有等待的异步函数,因此您只是创建了一个新的未处理的 Promise,并成功完成了第一个 Promise 的执行(因为您捕获了错误并忽略了它)。

您也不应该while在重试中使用循环,因为这将导致一个永无止境的循环,因为retry变量对于每个调用都是本地的。替换为if将修复它。

最后,如果所有重试都失败,您将无法引发原始错误(将在下面显示如何修复)。

我不建议使用递归来处理重试,我认为它会使代码的可读性降低,并且还有一些不必要的堆栈分配。但是,出于教育目的,让我们修复此代码:

const delay = (ms=1000)=>new Promise(resolve=>setTimeout(resolve, ms));

const readCustomerStatus = async (retry = 2) => {
    let content = '';
    try {
      // Notice we invoked function here to create a new Promise on each iteration
      await delay();
      content = await fs.readFileSync(CUSTOMER_PATH,'utf8');
    } catch(error) {
      if (retry > 0) {
        // it is important to use await here and assign the result to `content`
        content = await readCustomerStatus(retry - 1);
      } else {
        // This is our base case, so we just raise whatever error we found
        throw error;
      }
    }
    return content;
}

推荐阅读