Skip to content

Commit

Permalink
feat: add test.throws method
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Jun 26, 2023
1 parent b82ca2e commit c028dd6
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 1 deletion.
2 changes: 2 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ export function test(title: string, callback?: TestExecutor<TestContext, undefin
if (callback) {
testInstance.run(callback)
}

return testInstance
}

/**
Expand Down
81 changes: 81 additions & 0 deletions modules/core/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,20 @@ import {
Runner as BaseRunner,
TestContext as BaseTestContext,
} from '@japa/core'
import { inspect } from 'node:util'
import { AssertionError } from 'node:assert'
import { BaseReporter } from './reporters/base.js'
import type { DataSetNode, TestHooksCleanupHandler } from './types.js'

declare module '@japa/core' {
interface Test<Context extends Record<any, any>, TestData extends DataSetNode = undefined> {
throws(message: string | RegExp, errorConstructor?: any): this
}
interface TestContext {
cleanup: (cleanupCallback: TestHooksCleanupHandler<TestContext>) => void
}
}

export { Emitter, Refiner, BaseReporter }

/**
Expand Down Expand Up @@ -56,6 +67,76 @@ export class Test<TestData extends DataSetNode = undefined> extends BaseTest<
* @inheritdoc
*/
static executingCallbacks = []

/**
* Assert the test callback throws an exception when a certain
* error message and optionally is an instance of a given
* Error class.
*/
throws(message: string | RegExp, errorConstructor?: any) {
const errorInPoint = new AssertionError({})
const existingExecutor = this.options.executor
if (!existingExecutor) {
throw new Error('Cannot use "test.throws" method without a test callback')
}

/**
* Overwriting existing callback
*/
this.options.executor = async (...args: [any, any, any]) => {
let raisedException: any
try {
await existingExecutor(...args)
} catch (error) {
raisedException = error
}

/**
* Notify no exception has been raised
*/
if (!raisedException) {
errorInPoint.message = 'Expected test to throw an exception'
throw errorInPoint
}

/**
* Constructor mis-match
*/
if (errorConstructor && !(raisedException instanceof errorConstructor)) {
errorInPoint.message = `Expected test to throw "${inspect(errorConstructor)}"`
throw errorInPoint
}

/**
* Error does not have a message property
*/
const exceptionMessage: unknown = raisedException.message
if (!exceptionMessage || typeof exceptionMessage !== 'string') {
errorInPoint.message = 'Expected test to throw an exception with message property'
throw errorInPoint
}

/**
* Message does not match
*/
if (typeof message === 'string') {
if (exceptionMessage !== message) {
errorInPoint.message = `Expected test to throw "${message}". Instead received "${raisedException.message}"`
errorInPoint.actual = raisedException.message
errorInPoint.expected = message
throw errorInPoint
}
return
}

if (!message.test(exceptionMessage)) {
errorInPoint.message = `Expected test error to match "${message}" regular expression`
throw errorInPoint
}
}

return this
}
}

/**
Expand Down
108 changes: 107 additions & 1 deletion tests/runner.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { test } from 'node:test'
import { runner } from '../factories/main.js'
import { GlobalHooks } from '../src/hooks.js'
import { ConfigManager } from '../src/config_manager.js'
import { wrapAssertions } from '../tests_helpers/main.js'
import { pEvent, wrapAssertions } from '../tests_helpers/main.js'
import { createTest, createTestGroup } from '../src/create_test.js'
import { clearCache, getFailedTests, retryPlugin } from '../src/plugins/retry.js'
import { Emitter, Refiner, Runner, Suite } from '../modules/core/main.js'
Expand Down Expand Up @@ -93,6 +93,112 @@ test.describe('Runner | create tests and groups', () => {
assert.deepEqual(stack, ['executed'])
})
})

test('assert test throws an exception', async () => {
const emitter = new Emitter()
const refiner = new Refiner()
const t = createTest('', emitter, refiner, {})
t.run(() => {
throw new Error('Failed')
}).throws('Failed')

const [event] = await Promise.all([pEvent(emitter, 'test:end'), t.exec()])
assert.equal(event!.hasError, false)
})

test('assert error matches the regular expression', async () => {
const emitter = new Emitter()
const refiner = new Refiner()
const t = createTest('', emitter, refiner, {})
t.run(() => {
throw new Error('Failed')
}).throws(/ed?/)

const [event] = await Promise.all([pEvent(emitter, 'test:end'), t.exec()])
assert.equal(event!.hasError, false)
})

test('throw error when test does not have a callback defined', async () => {
const emitter = new Emitter()
const refiner = new Refiner()
const t = createTest('', emitter, refiner, {})

await wrapAssertions(() => {
assert.throws(
() => t.throws(/ed?/),
'Cannot use "test.throws" method without a test callback'
)
})
})

test('assert test throws an instance of a given class', async () => {
const emitter = new Emitter()
const refiner = new Refiner()
const t = createTest('', emitter, refiner, {})
t.run(() => {
throw new Error('Failed')
}).throws('Failed', Error)

const [event] = await Promise.all([pEvent(emitter, 'test:end'), t.exec()])
assert.equal(event!.hasError, false)
})

test('fail when test does not throw an exception', async () => {
const emitter = new Emitter()
const refiner = new Refiner()
const t = createTest('', emitter, refiner, {})
t.run(() => {}).throws('Failed', Error)

const [event] = await Promise.all([pEvent(emitter, 'test:end'), t.exec()])
assert.equal(event!.hasError, true)
assert.equal(event!.errors[0].error.message, 'Expected test to throw an exception')
})

test('fail when error constructor mismatch', async () => {
class Exception {}
const emitter = new Emitter()
const refiner = new Refiner()
const t = createTest('', emitter, refiner, {})
t.run(() => {
throw new Error('Failed')
}).throws('Failed', Exception)

const [event] = await Promise.all([pEvent(emitter, 'test:end'), t.exec()])
assert.equal(event!.hasError, true)
assert.equal(event!.errors[0].error.message, 'Expected test to throw "[class Exception]"')
})

test('fail when error message mismatch', async () => {
const emitter = new Emitter()
const refiner = new Refiner()
const t = createTest('', emitter, refiner, {})
t.run(() => {
throw new Error('Failed')
}).throws('Failure')

const [event] = await Promise.all([pEvent(emitter, 'test:end'), t.exec()])
assert.equal(event!.hasError, true)
assert.equal(
event!.errors[0].error.message,
'Expected test to throw "Failure". Instead received "Failed"'
)
})

test('fail when error does not match the regular expression', async () => {
const emitter = new Emitter()
const refiner = new Refiner()
const t = createTest('', emitter, refiner, {})
t.run(() => {
throw new Error('Failed')
}).throws(/lure?/)

const [event] = await Promise.all([pEvent(emitter, 'test:end'), t.exec()])
assert.equal(event!.hasError, true)
assert.equal(
event!.errors[0].error.message,
'Expected test error to match "/lure?/" regular expression'
)
})
})

test.describe('Runner | global hooks', () => {
Expand Down
24 changes: 24 additions & 0 deletions tests_helpers/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
*/

import { ErrorsPrinter } from '@japa/errors-printer'
import { Emitter } from '../modules/core/main.js'
import { RunnerEvents } from '../src/types.js'

export async function wrapAssertions(fn: () => void | Promise<void>) {
try {
Expand All @@ -17,3 +19,25 @@ export async function wrapAssertions(fn: () => void | Promise<void>) {
throw new Error('Assertion failure')
}
}

/**
* Promisify an event
*/
export function pEvent<Name extends keyof RunnerEvents>(
emitter: Emitter,
event: Name,
timeout: number = 500
) {
return new Promise<RunnerEvents[Name] | null>((resolve) => {
function handler(data: RunnerEvents[Name]) {
emitter.off(event, handler)
resolve(data)
}

setTimeout(() => {
emitter.off(event, handler)
resolve(null)
}, timeout)
emitter.on(event, handler)
})
}

0 comments on commit c028dd6

Please sign in to comment.