[Javascript] Test everything!

When you are not sure how an API/library call/language instruction works, you absolutely should add several tests on it.

  • It does not hurt your test performance much
  • It assures the expected behavior when the API changes, library updates, language compiler/interpreter upgrades.

Let me show an example with the ES6 Promise API. Many people first time see it would wonder: what will happen if the promise is resolved multiple times, rejected multiple times, rejected after resolved and vice versa, etc.

Instead of googling for the answer and remember the result. Sometime later, you forget it and google it again and again. Why not just add the following test to your project and confirm the result.

/* eslint-disable prefer-promise-reject-errors */
const flipPromise = require('flip-promise')

describe('promise', () => {
	test('error catch with resolve', () => new Promise(async (rs, rj) => {
		const getPromise = () => new Promise(resolve => {
			try {
				resolve()
			} catch (err) {
				rj('error caught in unexpected location')
			}
		})
		try {
			await getPromise()
			throw new Error('error thrown out side')
		} catch (e) {
			rs('error caught in expected location')
		}
	}))
	test('error catch with reject', () => new Promise(async (rs, rj) => {
		const getPromise = () => new Promise((_resolve, reject) => {
			try {
				reject()
			} catch (err) {
				rj('error caught in unexpected location')
			}
		})
		try {
			await getPromise()
		} catch (e) {
			try {
				throw new Error('error thrown out side')
			} catch (e){
				rs('error caught in expected location')
			}
		}
	}))
	test('await multiple times resolved promise', async () => {
		const pr = Promise.resolve(1)
		expect(await pr).toBe(1)
		expect(await pr).toBe(1)
	})
	test('await multiple times rejected promise', async () => {
		const pr = Promise.reject(1)
		expect(await flipPromise(pr)).toBe(1)
		expect(await flipPromise(pr)).toBe(1)
	})
	test('resolve multiple times', async () => {
		const pr = new Promise(resolve => {
			resolve(1)
			resolve(2)
			resolve(3)
		})
		expect(await pr).toBe(1)
	})
	test('resolve then reject', async () => {
		const pr = new Promise((resolve, reject) => {
			resolve(1)
			resolve(2)
			resolve(3)
			reject(4)
		})
		expect(await pr).toBe(1)
	})
	test('reject multiple times', async () => {
		const pr = new Promise((_resolve, reject) => {
			reject(1)
			reject(2)
			reject(3)
		})
		expect(await flipPromise(pr)).toBe(1)
	})

	test('reject then resolve', async () => {
		const pr = new Promise((resolve, reject) => {
			reject(1)
			reject(2)
			reject(3)
			resolve(4)
		})
		expect(await flipPromise(pr)).toBe(1)
	})

	test('constructor is not async', async () => {
		let val
		let val1
		const pr = new Promise(resolve => {
			val = 1
			setTimeout(() => {
				resolve()
				val1 = 2
			})
		})
		expect(val).toBe(1)
		expect(val1).toBeUndefined()
		await pr
		expect(val).toBe(1)
		expect(val1).toBe(2)
	})

})

This test proves several facts of the ES6, respectively

  • The resolve()/reject() call never throws an error.
  • Once settled (rejected), the resolved value (rejected error) will be preserved regardless of following resolve() or reject() calls.

I published this code here. You can download it, run it locally or check the running result here, tested with node version 8, 9, 10, 11, 12, 13.