Skip to content

Commit 4bda6e0

Browse files
MoLowtargos
authored andcommitted
test_runner: expose describe and it
PR-URL: #43420 Refs: #43415 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
1 parent d90a6f9 commit 4bda6e0

File tree

8 files changed

+1072
-64
lines changed

8 files changed

+1072
-64
lines changed

doc/api/test.md

+93-2
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,42 @@ test('skip() method with message', (t) => {
148148
});
149149
```
150150

151+
## `describe`/`it` syntax
152+
153+
Running tests can also be done using `describe` to declare a suite
154+
and `it` to declare a test.
155+
A suite is used to organize and group related tests together.
156+
`it` is an alias for `test`, except there is no test context passed,
157+
since nesting is done using suites, as demonstrated in this example
158+
159+
```js
160+
describe('A thing', () => {
161+
it('should work', () => {
162+
assert.strictEqual(1, 1);
163+
});
164+
165+
it('should be ok', () => {
166+
assert.strictEqual(2, 2);
167+
});
168+
169+
describe('a nested thing', () => {
170+
it('should work', () => {
171+
assert.strictEqual(3, 3);
172+
});
173+
});
174+
});
175+
```
176+
177+
`describe` and `it` are imported from the `node:test` module
178+
179+
```mjs
180+
import { describe, it } from 'node:test';
181+
```
182+
183+
```cjs
184+
const { describe, it } = require('node:test');
185+
```
186+
151187
### `only` tests
152188

153189
If Node.js is started with the [`--test-only`][] command-line option, it is
@@ -303,7 +339,7 @@ added: v18.0.0
303339
* `todo` {boolean|string} If truthy, the test marked as `TODO`. If a string
304340
is provided, that string is displayed in the test results as the reason why
305341
the test is `TODO`. **Default:** `false`.
306-
* `fn` {Function|AsyncFunction} The function under test. This first argument
342+
* `fn` {Function|AsyncFunction} The function under test. The first argument
307343
to this function is a [`TestContext`][] object. If the test uses callbacks,
308344
the callback function is passed as the second argument. **Default:** A no-op
309345
function.
@@ -335,6 +371,59 @@ test('top level test', async (t) => {
335371
});
336372
```
337373

374+
## `describe([name][, options][, fn])`
375+
376+
* `name` {string} The name of the suite, which is displayed when reporting test
377+
results. **Default:** The `name` property of `fn`, or `'<anonymous>'` if `fn`
378+
does not have a name.
379+
* `options` {Object} Configuration options for the suite.
380+
supports the same options as `test([name][, options][, fn])`
381+
* `fn` {Function} The function under suite.
382+
a synchronous function declaring all subtests and subsuites.
383+
**Default:** A no-op function.
384+
* Returns: `undefined`.
385+
386+
The `describe()` function imported from the `node:test` module. Each
387+
invocation of this function results in the creation of a Subtest
388+
and a test point in the TAP output.
389+
After invocation of top level `describe` functions,
390+
all top level tests and suites will execute
391+
392+
## `describe.skip([name][, options][, fn])`
393+
394+
Shorthand for skipping a suite, same as [`describe([name], { skip: true }[, fn])`][describe options].
395+
396+
## `describe.todo([name][, options][, fn])`
397+
398+
Shorthand for marking a suite as `TODO`, same as
399+
[`describe([name], { todo: true }[, fn])`][describe options].
400+
401+
## `it([name][, options][, fn])`
402+
403+
* `name` {string} The name of the test, which is displayed when reporting test
404+
results. **Default:** The `name` property of `fn`, or `'<anonymous>'` if `fn`
405+
does not have a name.
406+
* `options` {Object} Configuration options for the suite.
407+
supports the same options as `test([name][, options][, fn])`.
408+
* `fn` {Function|AsyncFunction} The function under test.
409+
If the test uses callbacks, the callback function is passed as an argument.
410+
**Default:** A no-op function.
411+
* Returns: `undefined`.
412+
413+
The `it()` function is the value imported from the `node:test` module.
414+
Each invocation of this function results in the creation of a test point in the
415+
TAP output.
416+
417+
## `it.skip([name][, options][, fn])`
418+
419+
Shorthand for skipping a test,
420+
same as [`it([name], { skip: true }[, fn])`][it options].
421+
422+
## `it.todo([name][, options][, fn])`
423+
424+
Shorthand for marking a test as `TODO`,
425+
same as [`it([name], { todo: true }[, fn])`][it options].
426+
338427
## Class: `TestContext`
339428

340429
<!-- YAML
@@ -449,7 +538,7 @@ added: v18.0.0
449538
* `todo` {boolean|string} If truthy, the test marked as `TODO`. If a string
450539
is provided, that string is displayed in the test results as the reason why
451540
the test is `TODO`. **Default:** `false`.
452-
* `fn` {Function|AsyncFunction} The function under test. This first argument
541+
* `fn` {Function|AsyncFunction} The function under test. The first argument
453542
to this function is a [`TestContext`][] object. If the test uses callbacks,
454543
the callback function is passed as the second argument. **Default:** A no-op
455544
function.
@@ -475,4 +564,6 @@ test('top level test', async (t) => {
475564
[`--test`]: cli.md#--test
476565
[`TestContext`]: #class-testcontext
477566
[`test()`]: #testname-options-fn
567+
[describe options]: #describename-options-fn
568+
[it options]: #testname-options-fn
478569
[test runner execution model]: #test-runner-execution-model

lib/internal/main/test_runner.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const {
2020
ERR_TEST_FAILURE,
2121
},
2222
} = require('internal/errors');
23-
const test = require('internal/test_runner/harness');
23+
const { test } = require('internal/test_runner/harness');
2424
const { kSubtestsFailed } = require('internal/test_runner/test');
2525
const {
2626
isSupportedFileType,

lib/internal/test_runner/harness.js

+60-27
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
'use strict';
2-
const { FunctionPrototypeBind, SafeMap } = primordials;
2+
const {
3+
ArrayPrototypeForEach,
4+
FunctionPrototypeBind,
5+
SafeMap,
6+
} = primordials;
37
const {
48
createHook,
59
executionAsyncId,
@@ -9,34 +13,43 @@ const {
913
ERR_TEST_FAILURE,
1014
},
1115
} = require('internal/errors');
12-
const { Test } = require('internal/test_runner/test');
16+
const { Test, ItTest, Suite } = require('internal/test_runner/test');
17+
18+
19+
const testResources = new SafeMap();
20+
const root = new Test({ __proto__: null, name: '<root>' });
21+
let wasRootSetup = false;
1322

14-
function createProcessEventHandler(eventName, rootTest, testResources) {
23+
function createProcessEventHandler(eventName, rootTest) {
1524
return (err) => {
1625
// Check if this error is coming from a test. If it is, fail the test.
1726
const test = testResources.get(executionAsyncId());
1827

19-
if (test !== undefined) {
20-
if (test.finished) {
21-
// If the test is already finished, report this as a top level
22-
// diagnostic since this is a malformed test.
23-
const msg = `Warning: Test "${test.name}" generated asynchronous ` +
24-
'activity after the test ended. This activity created the error ' +
25-
`"${err}" and would have caused the test to fail, but instead ` +
26-
`triggered an ${eventName} event.`;
28+
if (!test) {
29+
throw err;
30+
}
2731

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

32-
test.fail(new ERR_TEST_FAILURE(err, eventName));
33-
test.postRun();
40+
rootTest.diagnostic(msg);
41+
return;
3442
}
43+
44+
test.fail(new ERR_TEST_FAILURE(err, eventName));
45+
test.postRun();
3546
};
3647
}
3748

3849
function setup(root) {
39-
const testResources = new SafeMap();
50+
if (wasRootSetup) {
51+
return root;
52+
}
4053
const hook = createHook({
4154
init(asyncId, type, triggerAsyncId, resource) {
4255
if (resource instanceof Test) {
@@ -58,9 +71,9 @@ function setup(root) {
5871
hook.enable();
5972

6073
const exceptionHandler =
61-
createProcessEventHandler('uncaughtException', root, testResources);
74+
createProcessEventHandler('uncaughtException', root);
6275
const rejectionHandler =
63-
createProcessEventHandler('unhandledRejection', root, testResources);
76+
createProcessEventHandler('unhandledRejection', root);
6477

6578
process.on('uncaughtException', exceptionHandler);
6679
process.on('unhandledRejection', rejectionHandler);
@@ -113,19 +126,39 @@ function setup(root) {
113126

114127
root.reporter.pipe(process.stdout);
115128
root.reporter.version();
129+
130+
wasRootSetup = true;
131+
return root;
116132
}
117133

118134
function test(name, options, fn) {
119-
// If this is the first test encountered, bootstrap the test harness.
120-
if (this.subtests.length === 0) {
121-
setup(this);
135+
const subtest = setup(root).createSubtest(Test, name, options, fn);
136+
return subtest.start();
137+
}
138+
139+
function runInParentContext(Factory) {
140+
function run(name, options, fn, overrides) {
141+
const parent = testResources.get(executionAsyncId()) || setup(root);
142+
const subtest = parent.createSubtest(Factory, name, options, fn, overrides);
143+
if (parent === root) {
144+
subtest.start();
145+
}
122146
}
123147

124-
const subtest = this.createSubtest(name, options, fn);
148+
const cb = (name, options, fn) => {
149+
run(name, options, fn);
150+
};
125151

126-
return subtest.start();
152+
ArrayPrototypeForEach(['skip', 'todo'], (keyword) => {
153+
cb[keyword] = (name, options, fn) => {
154+
run(name, options, fn, { [keyword]: true });
155+
};
156+
});
157+
return cb;
127158
}
128159

129-
const root = new Test({ name: '<root>' });
130-
131-
module.exports = FunctionPrototypeBind(test, root);
160+
module.exports = {
161+
test: FunctionPrototypeBind(test, root),
162+
describe: runInParentContext(Suite),
163+
it: runInParentContext(ItTest),
164+
};

0 commit comments

Comments
 (0)