Exceptions
We all write tests that deal with exceptions and promise rejections. For example:
- Assert, a call to function
foothrows an exception. - Assert, the database insert statement for a duplicate value rejects the promise with a
Unique constraintexception.
Usually, you will wrap these function calls inside a try/catch statement and write assertions for the error object.
test('validate email format', ({ assert }) => {
try {
validateEmail('foo')
} catch (error) {
assert.equal(error.message, '"foo" is not a valid email address')
}
})
test('do not insert duplicate emails', ({ assert }) => {
await createUser({ email: 'foo@bar.com' })
try {
await createUser({ email: 'foo@bar.com' })
} catch (error) {
assert.matches(error.message, /Unique constraint/)
}
})
There are two problems with the try/catch statement.
- You will get a "false positive" test if the code inside the try block never raises an exception.
- The
try/catchstatement adds visual noise to your tests, especially when writing nested statements.
Using dedicated assertion methods
Both the assert and the expect plugins of Japa have dedicated methods to assert a function call throws an exception.
assert.throws
The assert.throws method accepts a function as the first parameter and the error message you expect as the second parameter.
This method only works with synchronous function calls.
test('validate email format', ({ assert }) => {
assert.throws(
() => validateEmail('foo'),
'"foo" is not a valid email address'
)
})
assert.rejects
Similar to the assert.throws, the assert.rejects method also accepts a function and the error message for assertion. However, in this case, the callback function must return a Promise.
test('do not insert duplicate emails', ({ assert }) => {
await createUser({ email: 'foo@bar.com' })
await assert.rejects(
async () => createUser({ email: 'foo@bar.com' }),
/Unique constraint/
)
})
expect.toThrow
The expect.toThrow method accepts a callback function and the error message for assertion. The callback function should be synchronous in nature.
test('validate email format', ({ expect }) => {
expect(() => validateEmail('foo'))
.toThrow('"foo" is not a valid email address')
})
expect.rejects
The expect.rejects matcher accepts the promise as the first parameter and allows you to chain other matchers like toThrow or toEqual.
test('do not insert duplicate emails', ({ expect }) => {
await createUser({ email: 'foo@bar.com' })
await expect(createUser({ email: 'foo@bar.com' }))
.rejects
.toThrow(/Unique constraint/)
})
High order assertion
An alternative API (independent of the assertion library) is to expect a test to throw an exception and write an assertion directly on the test using the test.throws method.
Let's re-write the above two examples with a high-order assertion.
test('validate email format', () => {
validateEmail('foo')
})
.throws('"foo" is not a valid email address')
test('do not insert duplicate emails', () => {
await createUser({ email: 'foo@bar.com' })
/**
* The second call will throw an exception, and
* there is no need to handle it within the test
* callback
*/
await createUser({ email: 'foo@bar.com' })
})
.throws(/Unique constraint/)