【问题标题】:Testing a Recursive polling function with jest and fake timers使用 jest 和 fake timer 测试递归轮询函数
【发布时间】:2019-02-24 07:56:29
【问题描述】:

我创建了一个轮询服务,它递归调用一个 api,如果满足某些条件,则该 api 成功时,会再次继续轮询。

/**
   * start a timer with the interval specified by the user || default interval
   * we are using setTimeout and not setinterval because a slow back end server might take more time than our interval time and that would lead to
   * a queue of ajax requests with no response at all.
   * -----------------------------------------
   * This function would call the api first time and only on the success response of the api we would poll again after the interval
   */
  runPolling() {
    const { url, onSuccess, onFailure, interval } = this.config;
    const _this = this;
    this.poll = setTimeout(() => {
      /* onSuccess would be handled by the user of service which would either return true or false
      * true - This means we need to continue polling
      * false - This means we need to stop polling
      */
      api
        .request(url)
        .then(response => {
          console.log('I was called', response);
          onSuccess(response);
        })
        .then(continuePolling => {
          _this.isPolling && continuePolling ? _this.runPolling() : _this.stopPolling();
        })
        .catch(error => {
          if (_this.config.shouldRetry && _this.config.retryCount > 0) {
            onFailure && onFailure(error);
            _this.config.retryCount--;
            _this.runPolling();
          } else {
            onFailure && onFailure(error);
            _this.stopPolling();
          }
        });
    }, interval);
  }

在尝试为其编写测试用例时,我不太确定如何模拟假计时器和 axios api 响应。

这是我目前所拥有的

import PollingService from '../PollingService';
import { statusAwaitingProduct } from '@src/__mock_data__/getSessionStatus';
import mockAxios from 'axios';

describe('timer events for runPoll', () => {
    let PollingObject,
    pollingInterval = 3000,
    url = '/session/status',
    onSuccess = jest.fn(() => {
      return false;
    });
    beforeAll(() => {
      PollingObject = new PollingService({
        url: url,
        interval: pollingInterval,
        onSuccess: onSuccess
      });
    });
    beforeEach(() => {
      jest.useFakeTimers();
    });
    test('runPolling should be called recursively when onSuccess returns true', async () => {
      expect.assertions(1);
      const mockedRunPolling = jest.spyOn(PollingObject, 'runPolling');
      const mockedOnSuccess = jest.spyOn(PollingObject.config, 'onSuccess');
      mockAxios.request.mockImplementation(
        () =>
          new Promise(resolve => {
            resolve(statusAwaitingProduct);
          })
      );

      PollingObject.startPolling();
      expect(mockedRunPolling).toHaveBeenCalledTimes(1);
      expect(setTimeout).toHaveBeenCalledTimes(1);
      expect(mockAxios.request).toHaveBeenCalledTimes(0);
      expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), pollingInterval);

      jest.runAllTimers();
      expect(mockAxios.request).toHaveBeenCalledTimes(1);
      expect(mockedOnSuccess).toHaveBeenCalledTimes(1);
      expect(PollingObject.isPolling).toBeTruthy();
      expect(mockedRunPolling).toHaveBeenCalledTimes(2);
    });
  });
});

这里即使调用了 mockedOnsuccess,但开玩笑的期望调用失败,说它被调用了 0 次而不是被调用了 1 次。

有人可以帮忙吗? 谢谢

【问题讨论】:

    标签: javascript jestjs


    【解决方案1】:

    问题

    您的测试也可能存在其他问题,但我将解决您提出的关于 expect(mockedOnSuccess).toHaveBeenCalledTimes(1); 失败并出现 0 times 的具体问题:

    jest.runAllTimers 将同步运行任何挂起的计时器回调,直到没有更多的剩余。这将执行在runPolling 内使用setTimeout 安排的匿名函数。当匿名函数执行时,它会调用api.request(url),但就是这样。匿名函数中的所有其他内容都包含在get queued in the PromiseJobs Jobs Queue introduced with ES6then 回调中。在jest.runAllTimers 返回并继续测试时,这些作业都不会执行。

    expect(mockAxios.request).toHaveBeenCalledTimes(1); 然后在 api.request(url) 执行后通过。

    expect(mockedOnSuccess).toHaveBeenCalledTimes(1); 然后失败,因为调用它的 then 回调仍在 PromiseJobs 队列中并且尚未执行。

    解决方案

    解决方案是确保在PromiseJobs 中排队的作业在断言mockedOnSuccess 被调用之前有机会运行。

    幸运的是,允许PromiseJobs 中的任何待处理作业在Jest 中的async 测试中运行非常容易,只需调用await Promise.resolve();。这实质上在PromiseJobs 结束时将其余测试排队,并允许队列中的任何待处理作业首先执行:

    test('runPolling should be called recursively when onSuccess returns true', async () => {
      ...
      jest.runAllTimers();
      await Promise.resolve();  // allow any pending jobs in PromiseJobs to execute
      expect(mockAxios.request).toHaveBeenCalledTimes(1);
      expect(mockedOnSuccess).toHaveBeenCalledTimes(1); // SUCCESS
      ...
    }
    

    请注意,理想情况下,异步函数将返回一个 Promise,然后测试可以等待。在您的情况下,您有一个使用 setTimeout 安排的回调,因此无法返回 Promise 以等待测试。

    另请注意,您有多个链接的 then 回调,因此您可能需要在测试期间多次等待 PromiseJobs 中的待处理作业。

    更多关于假定时器和 Promise 如何交互的细节here

    【讨论】:

    • 谢谢@brian-lives-outdoors。我有点遵循类似的方法,它对我有用。但我所做的正是你在这个答案中提到的。谢谢你的链接。我将通过它并阅读有关承诺和假计时器的更多信息。还有一个问题,如果我有多个 then 块所有链接,现在我必须编写多个 Promise.resolve 来访问所有这些 then 块。例如,如果我有三个然后链接,要达到第三个,那么我必须执行 Promise.resolve 三次。有没有更好的方法来做到这一点,或者这样做对吗?
    • 您可以通过将.then() 调用链接到Promise.resolve 的末尾来等待同一行上的多个PromiseJobs 循环。例如,要等待三个周期,请执行 await Promise.resolve().then().then();。再次重申,返回 Promises 并直接等待它们始终是理想的,但对于不可能的情况,这种方法将起作用。
    • 再次需要您的帮助。我已经用 fetch 替换了 axios 并再次遇到同样的问题。 onSuccess 没有被调用。除了这样,我可以通过任何方式与您联系,推特可能是?谢谢
    • @VivekN 只是创建一个新的 SO 问题,我会看看
    猜你喜欢
    • 1970-01-01
    • 2019-09-30
    • 2019-10-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-07-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多