用玩笑和假计时器测试递归轮询功能

Viv*_*ekN 3 javascript jestjs

我创建了一个轮询服务,它递归调用 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);
  }
Run Code Online (Sandbox Code Playgroud)

在尝试为其编写测试用例时,我不太确定如何模拟假计时器和 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);
    });
  });
});
Run Code Online (Sandbox Code Playgroud)

在这里,即使调用了 mockedOnsuccess 但 jest expect 调用失败,说它被调用了 0 次而不是被调用了 1 次。

有人可以帮忙吗?谢谢

Bri*_*ams 6

问题

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

jest.runAllTimers将同步运行任何挂起的计时器回调,直到没有更多的剩余。这将执行使用setTimeout内调度的匿名函数runPolling。当匿名函数执行时,它会调用,api.request(url)但仅而已。匿名函数中的其他所有内容都包含在then回调中,这些回调PromiseJobsES6 引入的作业队列中排队。到时间jest.runAllTimers返回时,这些作业都不会执行,测试将继续。

expect(mockAxios.request).toHaveBeenCalledTimes(1);然后通过,因为api.request(url)已经执行。

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

解决方案

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

幸运的是,允许任何挂起的作业在测试中PromiseJobs运行非常容易,只需调用. 这实质上是在测试结束时将其余的测试排队,并允许队列中的任何挂起作业首先执行:asyncJestawait 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
  ...
}
Run Code Online (Sandbox Code Playgroud)

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

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

有关虚假计时器和 Promise 如何交互的更多详细信息,请点击此处