测试用作setTimeout回调的异步函数

时间:2019-02-15 15:38:48

标签: javascript jestjs

我正在尝试为一个简单的轮询功能编写一个测试,该测试将检查API端点,直到获得200 OK响应并重试任何400500响应,直到maxAttempts。我在单元测试中遇到了麻烦,因为无论模拟响应如何,.then().catch()似乎从未执行过。

我要测试的功能。

const waitForSsl = ({
  onSuccess,
  onFailure,
  interval = 3,
  maxAttempts = 10,
}) => {
  const pingInterval = interval * 1000; // in seconds
  let attempts = 0;

  // TODO Add CertController to Laravel API to support this call.
  const ping = () => Axios.get('/status')
    .then((res) => { return onSuccess(); })
    .catch(() => {
      if (attempts < maxAttempts) {
        attempts += 1;
        setTimeout(ping, pingInterval);
      } else {
        onFailure();
      }
    });

  // Give server a chance to restart by waiting 5 seconds before starting pings.
  setTimeout(ping, 5000);
};

我可以验证该功能是否完全符合我的期望,但我希望自己进行省心的单元测试。

这是我第一次使用玩笑和罪恶之物

  it('Should only poll maxAttempts + 1 times', () => {
    jest.useFakeTimers();
    const onSuccessCallback = () => 'success!';
    const onFailureCallback = () => 'failed';
    const getStub = sinon.stub(Axios, 'get');
    getStub.rejects();

    ssl.waitForSsl({
      onSuccess: onSuccessCallback,
      onFailure: onFailureCallback,
      maxAttempts: 1,
    });

    expect(setTimeout).toHaveBeenCalledTimes(2);
  });

此测试失败,并显示错误Expected mock function to have been called two times, but it was called one time

我确实找到了这个post,但是该项目尚未使用异步/等待(ES8),仅在不等待的情况下调用Promise.resolve()不能解决问题。

我愿意使用moxios或jest.mock(),但我感到当在setTimeout中用作回呼时,无法测试已解决/已拒绝的诺言。一个工作单元测试以及对模拟工作原理的解释将是一个理想的答案。

1 个答案:

答案 0 :(得分:2)

这是一个很好的问题,因为它引起了人们对JavaScript某些独特特性及其幕后工作方式的关注。有关使用Timer Mocks时测试async代码的完整细分,请参见我的答案here


对于这个问题,重要的是要注意Timer Mocks replace functions like setTimeout with mocks要记住它们的名字。然后,在调用jest.advanceTimersByTime(或{{1}表示jest.runTimersToTime <22.0.0)时,Jest将运行在经过的时间内运行的所有内容。

请注意,Jest通常会为JavaScript消息队列安排一条消息,但是Timer Mocks会进行更改,以使所有内容都在当前正在执行的消息中运行


另一方面,当setTimeout解析或拒绝时,回调将安排在Promise Jobs队列中,该队列在当前消息完成之后,下一条消息开始之前运行

因此,任何Promise回调都有机会运行之前,任何当前正在运行的同步代码都将完成。


因此,在这种情况下,您需要调用Promise(或jest.advanceTimersByTime <22.0.0的jest.runTimersToTime)来运行按{{1} }。

棘手的部分是Jest函数在Promise Jobs队列中排队一个回调,直到当前同步消息完成后该回调才运行。

因此,您需要中断当前的同步消息,以允许Promise Jobs队列中的回调运行。通过使测试功能ping并在已解析的setTimeout上调用ping,最容易做到这一点,这实际上将其余测试排队在Promise Jobs队列的末尾,从而允许所有操作首先运行。


因此,要把它们放在一起,您的测试将需要交替增加时间并允许async回调像这样运行:

await