Tuesday, April 27, 2021

Javascript Tips and Tricks

  • It is not possible to reassign an argument in a method. (a) => { a = 2; } will not work.
    However, it is possible to reassign a property of an argument object in a method, so (a) => { a.value = 2; } will work.
  • Promise execution has a different queue than normal code. This can be important in testing async code: in the test we'll have to make sure the test execution waits for the promise to resolve, before testing the return value or side effect.
  • Optional chaining saves you the trouble of testing for undefined keys in nested objects. Eg. person.address?.street?.name 
  • Use a variable's value as a key in and object with this syntax: { [yourKeyVariable]: someValue } 
  • ...

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);
});

Monday, April 26, 2021

Testing the value of promises in Javascript without using Promise

Source: https://stackoverflow.com/a/54029876/4456532

Use case: when testing a code in Jest that already uses promises, I didn't want to use Promise resolve to peek into the promise, in order to not mess up the function execution. 

My helper function:
function promiseValue(p) {
	return process.binding('util').getPromiseDetails(p)[1];
}
Test for it with Jest:
test('promiseValue', () => {
	expect(promiseValue(new Promise(()=>{}))).toEqual(undefined);
	expect(promiseValue(Promise.resolve(2))).toEqual(2);
	expect(promiseValue(Promise.reject(0))).toEqual(0);
})

Testing the state of promises in JavaScript without using Promise

Relevant posts:

Use case: I would like to test a code with Jest that already uses promises, so to not mess up the promise queue, I needed a way to peek into the promise without using Promise.resolve or Promise.race.

My helper function:
const PROMISE_STATUS = Object.freeze({
	PENDING: 'pending',
	FULFILLED: 'fulfilled',
	REJECTED: 'rejected'
})

function promiseStatus(p) {
	if (inspect(p).includes(PROMISE_STATUS.PENDING)) {
		return PROMISE_STATUS.PENDING;
	}
	else if (inspect(p).includes(PROMISE_STATUS.REJECTED)) {
		return PROMISE_STATUS.REJECTED;
	}
	else {
		return PROMISE_STATUS.FULFILLED;
	}
}
Test for it with Jest:
test('promiseStatus', () => {
	expect(promiseStatus(new Promise(()=>{}))).toEqual(PROMISE_STATUS.PENDING);
	expect(promiseStatus(Promise.resolve(2))).toEqual(PROMISE_STATUS.FULFILLED);
	expect(promiseStatus(Promise.reject(0))).toEqual(PROMISE_STATUS.REJECTED);
})