Skip to content

Commit

Permalink
feat: expose describe and it
Browse files Browse the repository at this point in the history
PR-URL: nodejs/node#43420
Refs: nodejs/node#43415
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
  • Loading branch information
MoLow authored and aduh95 committed Jul 9, 2022
1 parent fc0256b commit e29cd3f
Show file tree
Hide file tree
Showing 9 changed files with 1,086 additions and 71 deletions.
95 changes: 93 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,42 @@ test('skip() method with message', t => {
})
```

## `describe`/`it` syntax

Running tests can also be done using `describe` to declare a suite
and `it` to declare a test.
A suite is used to organize and group related tests together.
`it` is an alias for `test`, except there is no test context passed,
since nesting is done using suites, as demonstrated in this example

```js
describe('A thing', () => {
it('should work', () => {
assert.strictEqual(1, 1);
});

it('should be ok', () => {
assert.strictEqual(2, 2);
});

describe('a nested thing', () => {
it('should work', () => {
assert.strictEqual(3, 3);
});
});
});
```

`describe` and `it` are imported from the `test` module

```mjs
import { describe, it } from 'test';
```

```cjs
const { describe, it } = require('test');
```

### `only` tests

If `node--test` is started with the `--test-only` command-line option, it is
Expand Down Expand Up @@ -303,7 +339,7 @@ internally.
- `todo` {boolean|string} If truthy, the test marked as `TODO`. If a string
is provided, that string is displayed in the test results as the reason why
the test is `TODO`. **Default:** `false`.
- `fn` {Function|AsyncFunction} The function under test. This first argument
- `fn` {Function|AsyncFunction} The function under test. The first argument
to this function is a [`TestContext`][] object. If the test uses callbacks,
the callback function is passed as the second argument. **Default:** A no-op
function.
Expand Down Expand Up @@ -335,6 +371,59 @@ test('top level test', async t => {
})
```

## `describe([name][, options][, fn])`

* `name` {string} The name of the suite, which is displayed when reporting test
results. **Default:** The `name` property of `fn`, or `'<anonymous>'` if `fn`
does not have a name.
* `options` {Object} Configuration options for the suite.
supports the same options as `test([name][, options][, fn])`
* `fn` {Function} The function under suite.
a synchronous function declaring all subtests and subsuites.
**Default:** A no-op function.
* Returns: `undefined`.

The `describe()` function imported from the `test` module. Each
invocation of this function results in the creation of a Subtest
and a test point in the TAP output.
After invocation of top level `describe` functions,
all top level tests and suites will execute

## `describe.skip([name][, options][, fn])`

Shorthand for skipping a suite, same as [`describe([name], { skip: true }[, fn])`][describe options].

## `describe.todo([name][, options][, fn])`

Shorthand for marking a suite as `TODO`, same as
[`describe([name], { todo: true }[, fn])`][describe options].

## `it([name][, options][, fn])`

* `name` {string} The name of the test, which is displayed when reporting test
results. **Default:** The `name` property of `fn`, or `'<anonymous>'` if `fn`
does not have a name.
* `options` {Object} Configuration options for the suite.
supports the same options as `test([name][, options][, fn])`.
* `fn` {Function|AsyncFunction} The function under test.
If the test uses callbacks, the callback function is passed as an argument.
**Default:** A no-op function.
* Returns: `undefined`.

The `it()` function is the value imported from the `test` module.
Each invocation of this function results in the creation of a test point in the
TAP output.

## `it.skip([name][, options][, fn])`

Shorthand for skipping a test,
same as [`it([name], { skip: true }[, fn])`][it options].

## `it.todo([name][, options][, fn])`

Shorthand for marking a test as `TODO`,
same as [`it([name], { todo: true }[, fn])`][it options].

## Class: `TestContext`

An instance of `TestContext` is passed to each test function in order to
Expand Down Expand Up @@ -394,7 +483,7 @@ execution of the test function. This function does not return a value.
- `todo` {boolean|string} If truthy, the test marked as `TODO`. If a string
is provided, that string is displayed in the test results as the reason why
the test is `TODO`. **Default:** `false`.
- `fn` {Function|AsyncFunction} The function under test. This first argument
- `fn` {Function|AsyncFunction} The function under test. The first argument
to this function is a [`TestContext`][] object. If the test uses callbacks,
the callback function is passed as the second argument. **Default:** A no-op
function.
Expand All @@ -406,6 +495,8 @@ behaves in the same fashion as the top level [`test()`][] function.
[tap]: https://testanything.org/
[`testcontext`]: #class-testcontext
[`test()`]: #testname-options-fn
[describe options]: #describename-options-fn
[it options]: #testname-options-fn
[test runner execution model]: #test-runner-execution-model

## License
Expand Down
4 changes: 2 additions & 2 deletions lib/internal/main/test_runner.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// https://github.com/nodejs/node/blob/1aab13cad9c800f4121c1d35b554b78c1b17bdbd/lib/internal/main/test_runner.js
// https://github.com/nodejs/node/blob/e2225ba8e1c00995c0f8bd56e607ea7c5b463ab9/lib/internal/main/test_runner.js
'use strict'
const {
ArrayFrom,
Expand All @@ -21,7 +21,7 @@ const {
ERR_TEST_FAILURE
}
} = require('#internal/errors')
const test = require('#internal/test_runner/harness')
const { test } = require('#internal/test_runner/harness')
const { kSubtestsFailed } = require('#internal/test_runner/test')
const {
isSupportedFileType,
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/per_context/primordials.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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.ArrayPrototypePush = (arr, ...el) => arr.push(...el)
exports.ArrayPrototypeReduce = (arr, fn, originalVal) => arr.reduce(fn, originalVal)
exports.ArrayPrototypeShift = arr => arr.shift()
exports.ArrayPrototypeSlice = (arr, offset) => arr.slice(offset)
exports.ArrayPrototypeSort = (arr, fn) => arr.sort(fn)
Expand All @@ -27,6 +28,7 @@ exports.ObjectIsExtensible = obj => Object.isExtensible(obj)
exports.ObjectPrototypeHasOwnProperty = (obj, property) => Object.prototype.hasOwnProperty.call(obj, property)
exports.ReflectApply = (target, self, args) => Reflect.apply(target, self, args)
exports.Promise = Promise
exports.PromiseResolve = val => Promise.resolve(val)
exports.SafeMap = Map
exports.SafeSet = Set
exports.SafeWeakMap = WeakMap
Expand Down
92 changes: 62 additions & 30 deletions lib/internal/test_runner/harness.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// https://github.com/nodejs/node/blob/1aab13cad9c800f4121c1d35b554b78c1b17bdbd/lib/internal/test_runner/harness.js

// https://github.com/nodejs/node/blob/e2225ba8e1c00995c0f8bd56e607ea7c5b463ab9/lib/internal/test_runner/harness.js
'use strict'

const { FunctionPrototypeBind, SafeMap } = require('#internal/per_context/primordials')
const {
ArrayPrototypeForEach,
FunctionPrototypeBind,
SafeMap
} = require('#internal/per_context/primordials')
const {
createHook,
executionAsyncId
Expand All @@ -12,34 +14,44 @@ const {
ERR_TEST_FAILURE
}
} = require('#internal/errors')
const { Test } = require('#internal/test_runner/test')
const { Test, ItTest, Suite } = require('#internal/test_runner/test')

function createProcessEventHandler (eventName, rootTest, testResources) {
const testResources = new SafeMap()
const root = new Test({ __proto__: null, name: '<root>' })
let wasRootSetup = false

function createProcessEventHandler (eventName, rootTest) {
return (err) => {
// Check if this error is coming from a test. If it is, fail the test.
const test = testResources.get(executionAsyncId())

if (test !== undefined) {
if (test.finished) {
// If the test is already finished, report this as a top level
// diagnostic since this is a malformed test.
const msg = `Warning: Test "${test.name}" generated asynchronous ` +
'activity after the test ended. This activity created the error ' +
`"${err}" and would have caused the test to fail, but instead ` +
`triggered an ${eventName} event.`
if (!test) {
// Node.js 14.x crashes if the error is throw here.
if (process.version.startsWith('v14.')) return
throw err
}

rootTest.diagnostic(msg)
return
}
if (test.finished) {
// If the test is already finished, report this as a top level
// diagnostic since this is a malformed test.
const msg = `Warning: Test "${test.name}" generated asynchronous ` +
'activity after the test ended. This activity created the error ' +
`"${err}" and would have caused the test to fail, but instead ` +
`triggered an ${eventName} event.`

test.fail(new ERR_TEST_FAILURE(err, eventName))
test.postRun()
rootTest.diagnostic(msg)
return
}

test.fail(new ERR_TEST_FAILURE(err, eventName))
test.postRun()
}
}

function setup (root) {
const testResources = new SafeMap()
if (wasRootSetup) {
return root
}
const hook = createHook({
init (asyncId, type, triggerAsyncId, resource) {
if (resource instanceof Test) {
Expand All @@ -61,9 +73,9 @@ function setup (root) {
hook.enable()

const exceptionHandler =
createProcessEventHandler('uncaughtException', root, testResources)
createProcessEventHandler('uncaughtException', root)
const rejectionHandler =
createProcessEventHandler('unhandledRejection', root, testResources)
createProcessEventHandler('unhandledRejection', root)

process.on('uncaughtException', exceptionHandler)
process.on('unhandledRejection', rejectionHandler)
Expand Down Expand Up @@ -116,19 +128,39 @@ function setup (root) {

root.reporter.pipe(process.stdout)
root.reporter.version()

wasRootSetup = true
return root
}

function test (name, options, fn) {
// If this is the first test encountered, bootstrap the test harness.
if (this.subtests.length === 0) {
setup(this)
const subtest = setup(root).createSubtest(Test, name, options, fn)
return subtest.start()
}

function runInParentContext (Factory) {
function run (name, options, fn, overrides) {
const parent = testResources.get(executionAsyncId()) || setup(root)
const subtest = parent.createSubtest(Factory, name, options, fn, overrides)
if (parent === root) {
subtest.start()
}
}

const subtest = this.createSubtest(name, options, fn)
const cb = (name, options, fn) => {
run(name, options, fn)
}

return subtest.start()
ArrayPrototypeForEach(['skip', 'todo'], (keyword) => {
cb[keyword] = (name, options, fn) => {
run(name, options, fn, { [keyword]: true })
}
})
return cb
}

const root = new Test({ name: '<root>' })

module.exports = FunctionPrototypeBind(test, root)
module.exports = {
test: FunctionPrototypeBind(test, root),
describe: runInParentContext(Suite),
it: runInParentContext(ItTest)
}
Loading

0 comments on commit e29cd3f

Please sign in to comment.