Skip to content

Commit

Permalink
test_runner: expose describe and it
Browse files Browse the repository at this point in the history
PR-URL: #43420
Refs: #43415
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
  • Loading branch information
MoLow authored and benjamingr committed Jun 29, 2022
1 parent d636fee commit e2225ba
Show file tree
Hide file tree
Showing 8 changed files with 1,072 additions and 64 deletions.
95 changes: 93 additions & 2 deletions doc/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,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 `node:test` module

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

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

### `only` tests

If Node.js is started with the [`--test-only`][] command-line option, it is
Expand Down Expand Up @@ -303,7 +339,7 @@ added: v18.0.0
* `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 `node: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 `node: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`

<!-- YAML
Expand Down Expand Up @@ -449,7 +538,7 @@ added: v18.0.0
* `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 @@ -475,4 +564,6 @@ test('top level test', async (t) => {
[`--test`]: cli.md#--test
[`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
2 changes: 1 addition & 1 deletion lib/internal/main/test_runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,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
87 changes: 60 additions & 27 deletions lib/internal/test_runner/harness.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
'use strict';
const { FunctionPrototypeBind, SafeMap } = primordials;
const {
ArrayPrototypeForEach,
FunctionPrototypeBind,
SafeMap,
} = primordials;
const {
createHook,
executionAsyncId,
Expand All @@ -9,34 +13,43 @@ const {
ERR_TEST_FAILURE,
},
} = require('internal/errors');
const { Test } = require('internal/test_runner/test');
const { Test, ItTest, Suite } = require('internal/test_runner/test');


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

function createProcessEventHandler(eventName, rootTest, testResources) {
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) {
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 @@ -58,9 +71,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 @@ -113,19 +126,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 e2225ba

Please sign in to comment.