Reputation: 1602
I have created a polling service which recursively calls an api and on success of the api if certain conditions are met, keeps polling again.
/**
* 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);
}
While trying to write the test cases for it, I am not very sure as to how can simulate fake timers and the axios api response.
This is what I have so far
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);
});
});
});
Here even though mockedOnsuccess is called but jest expect call fails saying it was called 0 times instead being called 1times.
Can someone please help? Thanks
Upvotes: 5
Views: 5841
Reputation: 45830
There might be other issues with your test as well, but I will address the specific question you asked about expect(mockedOnSuccess).toHaveBeenCalledTimes(1);
failing with 0 times
:
jest.runAllTimers
will synchronously run any pending timer callbacks until there are no more left. This will execute the anonymous function scheduled with setTimeout
within runPolling
. When the anonymous function executes it will call api.request(url)
but that is all that will happen. Everything else in the anonymous function is contained within then
callbacks that get queued in the PromiseJobs
Jobs Queue introduced with ES6. None of those jobs will have executed by the time jest.runAllTimers
returns and the test continues.
expect(mockAxios.request).toHaveBeenCalledTimes(1);
then passes since api.request(url)
has executed.
expect(mockedOnSuccess).toHaveBeenCalledTimes(1);
then fails since the then
callback that would have called it is still in the PromiseJobs
queue and hasn't executed yet.
The solution is to make sure the jobs queued in PromiseJobs
have a chance to run before asserting that mockedOnSuccess
was called.
Fortunately, it is very easy to allow any pending jobs in PromiseJobs
to run within an async
test in Jest
, just call await Promise.resolve();
. This essentially queues the rest of the test at the end of PromiseJobs
and allows any pending jobs in the queue to execute first:
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
...
}
Note that ideally an asynchronous function will return a Promise that a test can then wait for. In your case you have a callback scheduled with setTimeout
so there isn't a way to return a Promise for the test to wait on.
Also note that you have multiple chained then
callbacks so you may need to wait for the pending jobs in PromiseJobs
multiple times during your test.
More details about how fake timers and Promises interact here.
Upvotes: 7