【问题标题】:Jest mock timers not working as expected asynchronously; how to make this test pass?开玩笑的模拟计时器没有按预期异步工作;如何使这个测试通过?
【发布时间】:2019-12-23 08:09:53
【问题描述】:

我正在尝试测试一个可以进行调用并处理大量调度的排队组件。我想用一个模拟 api 来测试它,其中 api 响应会像在现实生活中一样被延迟,但我想使用模拟计时器并伪造时间的流逝。在下面的简单示例中,被测对象是 Caller 对象。

function mockCall(): Promise<string> {
    return new Promise<string>(resolve => setTimeout(() => resolve("success"), 20));
}

const callReceiver = jest.fn((result: string) => { console.log(result)});

class Caller {
    constructor(call: () => Promise<string>,
                receiver: (result: string) => void) {
        call().then(receiver);
    }
}

it("advances mock timers correctly", () => {
   jest.useFakeTimers();
   new Caller(mockCall, callReceiver);
   jest.advanceTimersByTime(50);
   expect(callReceiver).toHaveBeenCalled();
});

我认为这个测试应该通过,但是 expect 在计时器提前之前被评估,所以测试失败。如何编写此测试以使其通过?

顺便说一句,如果我使用真正的计时器并将expect 延迟超过 20 毫秒,则此测试确实通过了,但我特别感兴趣的是使用假计时器并通过代码推进时间,而不是等待实时时间过去.

【问题讨论】:

    标签: javascript typescript unit-testing timer jestjs


    【解决方案1】:

    您可以通过将 Promise 返回到 jest 来使测试工作,否则您的测试方法的执行已经完成并且不会等待 Promise 被实现。

    function mockCall() {
      return new Promise(resolve => setTimeout(() => resolve('success'), 20));
    }
    
    const callReceiver = jest.fn((result) => { console.log(result); });
    
    class Caller {
    
      constructor(callee, receiver) {
        this.callee = callee;
        this.receiver = receiver;
      }
    
      execute() {
        return this.callee().then(this.receiver);
      }
    }
    
    describe('my test suite', () => {
      it('advances mock timers correctly', () => {
        jest.useFakeTimers();
        const caller = new Caller(mockCall, callReceiver);
        const promise = caller.execute();
        jest.advanceTimersByTime(50);
    
        return promise.then(() => {
          expect(callReceiver).toHaveBeenCalled();
        });
      });
    });
    

    【讨论】:

      【解决方案2】:

      原因是mockCall 仍然返回 Promise,即使在你模拟了计时器之后。所以call().then() 将作为下一个微任务执行。要提前执行,您也可以将 expect 包装在微任务中:

      it("advances mock timers correctly", () => {
        jest.useFakeTimers();
        new Caller(mockCall, callReceiver);
        jest.advanceTimersByTime(50);
        return Promise.resolve().then(() => {
          expect(callReceiver).toHaveBeenCalled()
        });
      });
      

      小心返回这个 Promise,这样玩笑就会等到它完成。对我来说使用 async/await 会更好:

      it("advances mock timers correctly", async () => {
         jest.useFakeTimers();
         new Caller(mockCall, callReceiver);
         jest.advanceTimersByTime(50);
         await Promise.resolve();
         expect(callReceiver).toHaveBeenCalled();
      });
      

      顺便说一句,每次你模拟返回 Promise(例如 fetch)的东西时都是同样的事情 - 你需要像使用假计时器一样推进微任务队列。

      更多关于微任务/宏任务队列:https://abc.danch.me/microtasks-macrotasks-more-on-the-event-loop-881557d7af6f

      Jest repo 已公开提议以更清晰的方式处理待处理的 Promise https://github.com/facebook/jest/issues/2157,但目前还没有 ETA。

      【讨论】:

        猜你喜欢
        • 2021-08-18
        • 1970-01-01
        • 2018-11-13
        • 1970-01-01
        • 2021-02-25
        • 2018-08-17
        • 1970-01-01
        • 2021-06-16
        • 2021-10-12
        相关资源
        最近更新 更多