Exceptions
We all write tests that deal with exceptions and promise rejections. For example:
- Assert, a call to function
foo
throws an exception. - Assert, the database insert statement for a duplicate value rejects the promise with a
Unique constraint
exception.
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/catch
statement 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/)