简体   繁体   English

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

[英]Testing an async function used as the callback from setTimeout

I'm trying write a test for a simple polling function that checks an API endpoint until it gets a 200 OK response and retries on any 400 or 500 response up to maxAttempts. 我正在尝试为一个简单的轮询功能编写一个测试,该测试将检查API端点,直到获得200 OK响应并重试任何400500响应,直到maxAttempts。 I'm having trouble with the unit tests because the .then() and .catch() never seem to be executed regardless of the mock response. 我在与单元测试的麻烦,因为.then().catch()似乎从来不管模拟响应的执行。

The function I'm trying to test. 我要测试的功能。

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);
};

I can verify the function does exactly what I expect in the wild but I'd like a unit test for my own peace of mind. 我可以验证该功能是否确实符合我的期望,但是我希望自己进行省心的单元测试。

This is my first attempt using jest and sinon 这是我第一次使用玩笑和西农

  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);
  });

This test fails with the error Expected mock function to have been called two times, but it was called one time 该测试失败,并出现Expected mock function to have been called two times, but it was called one time错误的“ Expected mock function to have been called two times, but it was called one time ”错误Expected mock function to have been called two times, but it was called one time

I did find this post , but the project isn't using async/await (ES8) yet and just calling Promise.resolve() without await doesn't fix the issue. 我确实找到了这篇文章 ,但是该项目尚未使用async / await(ES8),仅在不等待的情况下调用Promise.resolve()不能解决问题。

I'm open to using, moxios, or jest.mock() but I'm getting the feeling there is no way to test a resolved/rejected promise when used as a callBack in setTimeout . 我愿意使用moxios或jest.mock()但我感觉当在setTimeout用作callBack时,无法测试已解决/已拒绝的诺言。 A working unit test and explanation of how that mocking works would be an ideal answer. 一个工作单元测试以及该模拟如何工作的解释将是一个理想的答案。

This is a good question because it draws attention to some unique characteristics of JavaScript and how it works under the hood. 这是一个很好的问题,因为它引起了人们对JavaScript某些独特特性及其在幕后如何工作的关注。 For a complete breakdown on testing async code while using Timer Mocks see my answer here . 有关测试完全崩溃async同时使用的代码计时器嘲笑看到我的答案在这里


For this question it is important to note that Timer Mocks replace functions like setTimeout with mocks that remember what they were called with. 对于这个问题,重要的是要注意, Timer Mocks用诸如记住它们被调用的内容的模拟代替了setTimeout功能 Then, when jest.advanceTimersByTime (or jest.runTimersToTime for Jest < 22.0.0) is called Jest runs everything that would have run in the elapsed time. 然后,当jest.advanceTimersByTime (或jest.runTimersToTimeJest <22.0.0)被称为Jest运行,将在所经过的时间已经运行了一切。

Note that setTimeout typically schedules a message for the JavaScript message queue, but Timer Mocks changes that so everything runs within the current executing message . 请注意, setTimeout通常为JavaScript消息队列安排一条消息,但是Timer Mocks会进行更改,以便所有内容都在当前正在执行的消息中运行。


On the other hand, when a Promise resolves or rejects, the callback gets scheduled in the Promise Jobs queue which runs after the current message completes and before the next message begins . 另一方面,当Promise解决或拒绝时,将在Promise Jobs队列中安排回调,该队列在当前消息完成之后,下一条消息开始之前运行。

So any currently running synchronous code will complete before any Promise callbacks have a chance to run. 因此,任何正在运行的同步代码都将在Promise回调有机会运行之前完成。


So in this case, you need to call jest.advanceTimersByTime (or jest.runTimersToTime for Jest < 22.0.0) to run the ping call scheduled with setTimeout . 因此,在这种情况下,你需要调用jest.advanceTimersByTime (或jest.runTimersToTimeJest <22.0.0)运行ping通话计划与setTimeout

The tricky part is that the ping function queues a callback in the Promise Jobs queue, which won't run until the current synchronous message completes. 棘手的部分是ping函数在Promise Jobs队列中排队一个回调,直到当前同步消息完成后才运行。

So you then need to interrupt the current synchronous message to allow the callback in the Promise Jobs queue to run. 因此,您需要中断当前的同步消息,以允许Promise Jobs队列中的回调运行。 This is most easily done by making your test function async and calling await on a resolved Promise , which essentially queues the rest of the test at the end of the Promise Jobs queue allowing everything before it to run first. 最简单的方法是使测试函数async并在已解决的Promise上调用await ,这实际上将其余测试排入Promise Jobs队列的末尾,从而允许所有内容先运行。


So, to bring it all together, your test will need to alternate advancing the time and allowing the Promise callbacks to run like this: 因此,将所有内容整合在一起,您的测试将需要交替增加时间并允许Promise回调像这样运行:

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

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

  for (let i = 0; i < maxAttempts; i++) {
    jest.advanceTimersByTime(5000);  // advance the time
    await Promise.resolve();  // allow queued Promise callbacks to run
  }
  expect(setTimeout).toHaveBeenCalledTimes(2);  // SUCCESS
});

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM