Tuesday, April 27, 2021

Testing a function with setTimeout and promises in Jest in Javascript

Inspiration for the code taken from Jest timer mocks documentation

Given a function that has both async/await and timeout, to test it with Jest, we need to know the following:
  • use jest.useFakeTimers() to get control of the timing
  • for each timeout, use the Jest Timer Control that applies
    • jest.runAllTimers() -- Fast-forward until all timers have been executed
    • jest.runOnlyPendingTimers() -- Fast forward and exhaust only currently pending timers (but not any new timers that get created during that process)
    • jest.advanceTimersByTime(1000) -- Fast-forward the given amount of milliseconds
  • for each await in the tested function, add await Promise.resolve() in the test, or any other way to resolve the promise before the test execution continues


The function to be tested:
async function infiniteAsyncTimerGame (beginningCallback, endingCallback) {
	console.log('start round');
	beginningCallback && await beginningCallback();
	console.log('awaited beginningCallback, will now set timeout');
	setTimeout(async () => {
		console.log('continuing after timeout');
		endingCallback && await endingCallback();
		console.log('awaited endingCallback, will now call function again');
		infiniteAsyncTimerGame(beginningCallback, endingCallback);
	}, 10000);
	console.log('end round');
}
The test:
test('infiniteAsyncTimerGame', async () => {
	// Preparations
	jest.useFakeTimers();

	let beginCounter = 0;
	let endCounter = 0;

	async function beginningCallback () {
		await new Promise(resolve => resolve(++beginCounter));
	}

	async function endingCallback () {
		await new Promise(resolve => resolve(++endCounter));
	}

	// Begin testing
	// notice the await keyword in front of the method call
	await infiniteAsyncTimerGame(beginningCallback, endingCallback);

	// At this point in time, there should have been a call to beginningCallback
	// We need to trigger the processing of each `await` keyword in the tested code
	// with `await Promise.resolve()` in the test run to flush the Promise queue
	// after that we can check for the value the Promise returned
	await Promise.resolve();
	console.log('check beginCounter');
	expect(beginCounter).toEqual(1);
	// After the beginningCallback, there should have been a single call to
	// setTimeout to schedule the next round in 10 seconds
	console.log('check timeout');
	expect(setTimeout).toHaveBeenCalledTimes(1);
	expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 10000);

	// Fast forward and exhaust only currently pending timers
	// (but not any new timers that get created during that process)
	console.log('runOnlyPendingTimers');
	jest.runOnlyPendingTimers();

	// At this point in time, there should have been a call to endingCallback
	await Promise.resolve();
	console.log('check endCounter');
	expect(endCounter).toEqual(1);

	// After the endingCallback, the next round should be started
	// with a new call to beginningCallback
	await Promise.resolve();
	console.log('check beginCounter');
	expect(beginCounter).toEqual(2);
	console.log('check timeout');
	expect(setTimeout).toHaveBeenCalledTimes(2);
	expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 10000);
	console.log('runOnlyPendingTimers');
	jest.runOnlyPendingTimers();
	await Promise.resolve();
	console.log('check endCounter');
	expect(endCounter).toEqual(2);

	// third round
	await Promise.resolve();
	console.log('check beginCounter');
	expect(beginCounter).toEqual(3);
	console.log('check timeout');
	expect(setTimeout).toHaveBeenCalledTimes(3);
	expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 10000);
	console.log('runOnlyPendingTimers');
	jest.runOnlyPendingTimers();
	await Promise.resolve();
	console.log('check endCounter');
	expect(endCounter).toEqual(3);
});

No comments:

Post a Comment