Skip to content

Commit

Permalink
feat: add before/after/each hooks
Browse files Browse the repository at this point in the history
PR-URL: nodejs/node#43730
Fixes: nodejs/node#43403
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
(cherry picked from commit 659dc126932f986fc33c7f1c878cb2b57a1e2fac)
  • Loading branch information
MoLow authored and aduh95 committed Jul 30, 2022
1 parent 215621e commit 4ed5d1f
Show file tree
Hide file tree
Showing 12 changed files with 696 additions and 40 deletions.
162 changes: 162 additions & 0 deletions README.md
Expand Up @@ -440,12 +440,166 @@ same as [`it([name], { skip: true }[, fn])`][it options].
Shorthand for marking a test as `TODO`,
same as [`it([name], { todo: true }[, fn])`][it options].

### `before([, fn][, options])`

* `fn` {Function|AsyncFunction} The hook function.
If the hook uses callbacks,
the callback function is passed as the second argument. **Default:** A no-op
function.
* `options` {Object} Configuration options for the hook. The following
properties are supported:
* `signal` {AbortSignal} Allows aborting an in-progress hook
* `timeout` {number} A number of milliseconds the hook will fail after.
If unspecified, subtests inherit this value from their parent.
**Default:** `Infinity`.

This function is used to create a hook running before running a suite.

```js
describe('tests', async () => {
before(() => console.log('about to run some test'));
it('is a subtest', () => {
assert.ok('some relevant assertion here');
});
});
```

### `after([, fn][, options])`

* `fn` {Function|AsyncFunction} The hook function.
If the hook uses callbacks,
the callback function is passed as the second argument. **Default:** A no-op
function.
* `options` {Object} Configuration options for the hook. The following
properties are supported:
* `signal` {AbortSignal} Allows aborting an in-progress hook
* `timeout` {number} A number of milliseconds the hook will fail after.
If unspecified, subtests inherit this value from their parent.
**Default:** `Infinity`.

This function is used to create a hook running after running a suite.

```js
describe('tests', async () => {
after(() => console.log('finished running tests'));
it('is a subtest', () => {
assert.ok('some relevant assertion here');
});
});
```

### `beforeEach([, fn][, options])`

* `fn` {Function|AsyncFunction} The hook function.
If the hook uses callbacks,
the callback function is passed as the second argument. **Default:** A no-op
function.
* `options` {Object} Configuration options for the hook. The following
properties are supported:
* `signal` {AbortSignal} Allows aborting an in-progress hook
* `timeout` {number} A number of milliseconds the hook will fail after.
If unspecified, subtests inherit this value from their parent.
**Default:** `Infinity`.

This function is used to create a hook running
before each subtest of the current suite.

```js
describe('tests', async () => {
beforeEach(() => t.diagnostics('about to run a test'));
it('is a subtest', () => {
assert.ok('some relevant assertion here');
});
});
```

### `afterEach([, fn][, options])`

* `fn` {Function|AsyncFunction} The hook function.
If the hook uses callbacks,
the callback function is passed as the second argument. **Default:** A no-op
function.
* `options` {Object} Configuration options for the hook. The following
properties are supported:
* `signal` {AbortSignal} Allows aborting an in-progress hook
* `timeout` {number} A number of milliseconds the hook will fail after.
If unspecified, subtests inherit this value from their parent.
**Default:** `Infinity`.

This function is used to create a hook running
after each subtest of the current test.

```js
describe('tests', async () => {
afterEach(() => t.diagnostics('about to run a test'));
it('is a subtest', () => {
assert.ok('some relevant assertion here');
});
});
```

## Class: `TestContext`

An instance of `TestContext` is passed to each test function in order to
interact with the test runner. However, the `TestContext` constructor is not
exposed as part of the API.

### `context.beforeEach([, fn][, options])`

* `fn` {Function|AsyncFunction} The hook function. The first argument
to this function is a [`TestContext`][] object. If the hook uses callbacks,
the callback function is passed as the second argument. **Default:** A no-op
function.
* `options` {Object} Configuration options for the hook. The following
properties are supported:
* `signal` {AbortSignal} Allows aborting an in-progress hook
* `timeout` {number} A number of milliseconds the hook will fail after.
If unspecified, subtests inherit this value from their parent.
**Default:** `Infinity`.

This function is used to create a hook running
before each subtest of the current test.

```js
test('top level test', async (t) => {
t.beforeEach((t) => t.diagnostics(`about to run ${t.name}`));
await t.test(
'This is a subtest',
(t) => {
assert.ok('some relevant assertion here');
}
);
});
```

### `context.afterEach([, fn][, options])`

* `fn` {Function|AsyncFunction} The hook function. The first argument
to this function is a [`TestContext`][] object. If the hook uses callbacks,
the callback function is passed as the second argument. **Default:** A no-op
function.
* `options` {Object} Configuration options for the hook. The following
properties are supported:
* `signal` {AbortSignal} Allows aborting an in-progress hook
* `timeout` {number} A number of milliseconds the hook will fail after.
If unspecified, subtests inherit this value from their parent.
**Default:** `Infinity`.

This function is used to create a hook running
after each subtest of the current test.

```js
test('top level test', async (t) => {
t.afterEach((t) => t.diagnostics(`finished running ${t.name}`));
await t.test(
'This is a subtest',
(t) => {
assert.ok('some relevant assertion here');
}
);
});
```

### `context.diagnostic(message)`

- `message` {string} Message to be displayed as a TAP diagnostic.
Expand All @@ -454,6 +608,10 @@ This function is used to write TAP diagnostics to the output. Any diagnostic
information is included at the end of the test's results. This function does
not return a value.

`context.name`

The name of the test

### `context.runOnly(shouldRunOnlyTests)`

- `shouldRunOnlyTests` {boolean} Whether or not to run `only` tests.
Expand Down Expand Up @@ -528,6 +686,10 @@ An instance of `SuiteContext` is passed to each suite function in order to
interact with the test runner. However, the `SuiteContext` constructor is not
exposed as part of the API.

### `context.name`

The name of the suite

### `context.signal`

* [`AbortSignal`][] Can be used to abort test subtasks when the test has been aborted.
Expand Down
15 changes: 15 additions & 0 deletions lib/internal/errors.js
Expand Up @@ -16,8 +16,10 @@ const {
ReflectApply,
SafeMap,
SafeWeakMap,
StringPrototypeIncludes,
StringPrototypeMatch,
StringPrototypeStartsWith,
StringPrototypeSlice,
Symbol,
SymbolFor
} = require('#internal/per_context/primordials')
Expand Down Expand Up @@ -362,6 +364,19 @@ E('ERR_TEST_FAILURE', function (error, failureType) {
E('ERR_INVALID_ARG_TYPE',
(name, expected, actual) => `Expected ${name} to be ${expected}, got type ${typeof actual}`,
TypeError)
E('ERR_INVALID_ARG_VALUE', (name, value, reason = 'is invalid') => {
let inspected
try {
inspected = String(value)
} catch {
inspected = `type ${typeof value}`
}
if (inspected.length > 128) {
inspected = `${StringPrototypeSlice(inspected, 0, 128)}...`
}
const type = StringPrototypeIncludes(name, '.') ? 'property' : 'argument'
return `The ${type} '${name}' ${reason}. Received ${inspected}`
}, TypeError, RangeError)
E('ERR_OUT_OF_RANGE',
(name, expected, actual) => `Expected ${name} to be ${expected}, got ${actual}`,
RangeError)
4 changes: 4 additions & 0 deletions lib/internal/per_context/primordials.js
Expand Up @@ -7,6 +7,7 @@ exports.ArrayPrototypeFilter = (arr, fn) => arr.filter(fn)
exports.ArrayPrototypeForEach = (arr, fn, thisArg) => arr.forEach(fn, thisArg)
exports.ArrayPrototypeIncludes = (arr, el, fromIndex) => arr.includes(el, fromIndex)
exports.ArrayPrototypeJoin = (arr, str) => arr.join(str)
exports.ArrayPrototypeMap = (arr, mapFn) => arr.map(mapFn)
exports.ArrayPrototypePush = (arr, ...el) => arr.push(...el)
exports.ArrayPrototypeReduce = (arr, fn, originalVal) => arr.reduce(fn, originalVal)
exports.ArrayPrototypeShift = arr => arr.shift()
Expand All @@ -27,6 +28,7 @@ exports.ObjectFreeze = obj => Object.freeze(obj)
exports.ObjectGetOwnPropertyDescriptor = (obj, key) => Object.getOwnPropertyDescriptor(obj, key)
exports.ObjectIsExtensible = obj => Object.isExtensible(obj)
exports.ObjectPrototypeHasOwnProperty = (obj, property) => Object.prototype.hasOwnProperty.call(obj, property)
exports.ObjectSeal = (obj) => Object.seal(obj)
exports.ReflectApply = (target, self, args) => Reflect.apply(target, self, args)
exports.Promise = Promise
exports.PromiseAll = iterator => Promise.all(iterator)
Expand All @@ -39,11 +41,13 @@ exports.SafePromiseAll = (array, mapFn) => Promise.all(mapFn ? array.map(mapFn)
exports.SafePromiseRace = (array, mapFn) => Promise.race(mapFn ? array.map(mapFn) : array)
exports.SafeSet = Set
exports.SafeWeakMap = WeakMap
exports.StringPrototypeIncludes = (str, needle) => str.includes(needle)
exports.StringPrototypeMatch = (str, reg) => str.match(reg)
exports.StringPrototypeReplace = (str, search, replacement) =>
str.replace(search, replacement)
exports.StringPrototypeReplaceAll = replaceAll
exports.StringPrototypeStartsWith = (haystack, needle, index) => haystack.startsWith(needle, index)
exports.StringPrototypeSlice = (str, ...args) => str.slice(...args)
exports.StringPrototypeSplit = (str, search, limit) => str.split(search, limit)
exports.Symbol = Symbol
exports.SymbolFor = repr => Symbol.for(repr)
Expand Down
15 changes: 13 additions & 2 deletions lib/internal/test_runner/harness.js
@@ -1,4 +1,4 @@
// https://github.com/nodejs/node/blob/26e27424ad91c60a44d3d4c58b62a39b555ba75d/lib/internal/test_runner/harness.js
// https://github.com/nodejs/node/blob/659dc126932f986fc33c7f1c878cb2b57a1e2fac/lib/internal/test_runner/harness.js
'use strict'
const {
ArrayPrototypeForEach,
Expand Down Expand Up @@ -177,8 +177,19 @@ function runInParentContext (Factory) {
return cb
}

function hook (hook) {
return (fn, options) => {
const parent = testResources.get(executionAsyncId()) || setup(root)
parent.createHook(hook, fn, options)
}
}

module.exports = {
test: FunctionPrototypeBind(test, root),
describe: runInParentContext(Suite),
it: runInParentContext(ItTest)
it: runInParentContext(ItTest),
before: hook('before'),
after: hook('after'),
beforeEach: hook('beforeEach'),
afterEach: hook('afterEach')
}

0 comments on commit 4ed5d1f

Please sign in to comment.