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....

问题:

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. I'm having trouble with the unit tests because the .then() and .catch() never seem to be executed regardless of the mock response.

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

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.

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. A working unit test and explanation of how that mocking works would be an ideal answer.


回答1:

This is a good question because it draws attention to some unique characteristics of JavaScript and how it works under the hood. For a complete breakdown on testing async code while using Timer Mocks see my answer here.


For this question it is important to note that Timer Mocks replace functions like setTimeout with mocks that remember what they were called with. 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.

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.


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.

So any currently running synchronous code will complete before any Promise callbacks have a chance to run.


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.

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.

So you then need to interrupt the current synchronous message to allow the callback in the Promise Jobs queue to run. 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.


So, to bring it all together, your test will need to alternate advancing the time and allowing the Promise callbacks to run like this:

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
});
  • 发表于 2019-02-17 06:54
  • 阅读 ( 350 )
  • 分类:sof

条评论

请先 登录 后评论
不写代码的码农
小编

篇文章

作家榜 »

  1. 小编 文章
返回顶部
部分文章转自于网络,若有侵权请联系我们删除