When you are not sure how a 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 latter, you forget it and google it again and again. Why not just add following test to your project and confirm the result.

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

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 throw 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 locally or check the running result here, tested with node version 8, 9, 10, 11, 12, 13.