Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 2f59529

Browse filesBrowse files
cjihrigmarco-ippolito
authored andcommitted
test_runner: support test plans
Co-Authored-By: Marco Ippolito <marcoippolito54@gmail.com> PR-URL: #52860 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it>
1 parent f74beb5 commit 2f59529
Copy full SHA for 2f59529

File tree

Expand file treeCollapse file tree

6 files changed

+321
-4
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

6 files changed

+321
-4
lines changed
Open diff view settings
Collapse file

‎doc/api/test.md‎

Copy file name to clipboardExpand all lines: doc/api/test.md
+57-1Lines changed: 57 additions & 1 deletion
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,10 @@ changes:
13641364
* `timeout` {number} A number of milliseconds the test will fail after.
13651365
If unspecified, subtests inherit this value from their parent.
13661366
**Default:** `Infinity`.
1367+
* `plan` {number} The number of assertions and subtests expected to be run in the test.
1368+
If the number of assertions run in the test does not match the number
1369+
specified in the plan, the test will fail.
1370+
**Default:** `undefined`.
13671371
* `fn` {Function|AsyncFunction} The function under test. The first argument
13681372
to this function is a [`TestContext`][] object. If the test uses callbacks,
13691373
the callback function is passed as the second argument. **Default:** A no-op
@@ -2965,6 +2969,54 @@ added:
29652969

29662970
The name of the test.
29672971

2972+
### `context.plan(count)`
2973+
2974+
<!-- YAML
2975+
added:
2976+
- REPLACEME
2977+
-->
2978+
2979+
> Stability: 1 - Experimental
2980+
2981+
* `count` {number} The number of assertions and subtests that are expected to run.
2982+
2983+
This function is used to set the number of assertions and subtests that are expected to run
2984+
within the test. If the number of assertions and subtests that run does not match the
2985+
expected count, the test will fail.
2986+
2987+
> Note: To make sure assertions are tracked, `t.assert` must be used instead of `assert` directly.
2988+
2989+
```js
2990+
test('top level test', (t) => {
2991+
t.plan(2);
2992+
t.assert.ok('some relevant assertion here');
2993+
t.subtest('subtest', () => {});
2994+
});
2995+
```
2996+
2997+
When working with asynchronous code, the `plan` function can be used to ensure that the
2998+
correct number of assertions are run:
2999+
3000+
```js
3001+
test('planning with streams', (t, done) => {
3002+
function* generate() {
3003+
yield 'a';
3004+
yield 'b';
3005+
yield 'c';
3006+
}
3007+
const expected = ['a', 'b', 'c'];
3008+
t.plan(expected.length);
3009+
const stream = Readable.from(generate());
3010+
stream.on('data', (chunk) => {
3011+
t.assert.strictEqual(chunk, expected.shift());
3012+
});
3013+
3014+
stream.on('end', () => {
3015+
done();
3016+
});
3017+
});
3018+
```
3019+
29683020
### `context.runOnly(shouldRunOnlyTests)`
29693021

29703022
<!-- YAML
@@ -3095,6 +3147,10 @@ changes:
30953147
* `timeout` {number} A number of milliseconds the test will fail after.
30963148
If unspecified, subtests inherit this value from their parent.
30973149
**Default:** `Infinity`.
3150+
* `plan` {number} The number of assertions and subtests expected to be run in the test.
3151+
If the number of assertions run in the test does not match the number
3152+
specified in the plan, the test will fail.
3153+
**Default:** `undefined`.
30983154
* `fn` {Function|AsyncFunction} The function under test. The first argument
30993155
to this function is a [`TestContext`][] object. If the test uses callbacks,
31003156
the callback function is passed as the second argument. **Default:** A no-op
@@ -3108,7 +3164,7 @@ behaves in the same fashion as the top level [`test()`][] function.
31083164
test('top level test', async (t) => {
31093165
await t.test(
31103166
'This is a subtest',
3111-
{ only: false, skip: false, concurrency: 1, todo: false },
3167+
{ only: false, skip: false, concurrency: 1, todo: false, plan: 4 },
31123168
(t) => {
31133169
assert.ok('some relevant assertion here');
31143170
},
Collapse file

‎lib/internal/test_runner/runner.js‎

Copy file name to clipboardExpand all lines: lib/internal/test_runner/runner.js
+2-1Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@ function run(options = kEmptyObject) {
462462
watch,
463463
setup,
464464
only,
465+
plan,
465466
} = options;
466467

467468
if (files != null) {
@@ -534,7 +535,7 @@ function run(options = kEmptyObject) {
534535
});
535536
}
536537

537-
const root = createTestTree({ __proto__: null, concurrency, timeout, signal });
538+
const root = createTestTree({ __proto__: null, concurrency, timeout, signal, plan });
538539
root.harness.shouldColorizeTestFiles ||= shouldColorizeTestFiles(root);
539540

540541
if (process.env.NODE_TEST_CONTEXT !== undefined) {
Collapse file

‎lib/internal/test_runner/test.js‎

Copy file name to clipboardExpand all lines: lib/internal/test_runner/test.js
+77-2Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const {
1212
MathMax,
1313
Number,
1414
ObjectDefineProperty,
15+
ObjectEntries,
1516
ObjectSeal,
1617
PromisePrototypeThen,
1718
PromiseResolve,
@@ -88,6 +89,7 @@ const {
8889
testOnlyFlag,
8990
} = parseCommandLine();
9091
let kResistStopPropagation;
92+
let assertObj;
9193
let findSourceMap;
9294
let noopTestStream;
9395

@@ -101,6 +103,19 @@ function lazyFindSourceMap(file) {
101103
return findSourceMap(file);
102104
}
103105

106+
function lazyAssertObject() {
107+
if (assertObj === undefined) {
108+
assertObj = new SafeMap();
109+
const assert = require('assert');
110+
for (const { 0: key, 1: value } of ObjectEntries(assert)) {
111+
if (typeof value === 'function') {
112+
assertObj.set(value, key);
113+
}
114+
}
115+
}
116+
return assertObj;
117+
}
118+
104119
function stopTest(timeout, signal) {
105120
const deferred = createDeferredPromise();
106121
const abortListener = addAbortListener(signal, deferred.resolve);
@@ -153,7 +168,25 @@ function testMatchesPattern(test, patterns) {
153168
);
154169
}
155170

171+
class TestPlan {
172+
constructor(count) {
173+
validateUint32(count, 'count', 0);
174+
this.expected = count;
175+
this.actual = 0;
176+
}
177+
178+
check() {
179+
if (this.actual !== this.expected) {
180+
throw new ERR_TEST_FAILURE(
181+
`plan expected ${this.expected} assertions but received ${this.actual}`,
182+
kTestCodeFailure,
183+
);
184+
}
185+
}
186+
}
187+
156188
class TestContext {
189+
#assert;
157190
#test;
158191

159192
constructor(test) {
@@ -180,6 +213,36 @@ class TestContext {
180213
this.#test.diagnostic(message);
181214
}
182215

216+
plan(count) {
217+
if (this.#test.plan !== null) {
218+
throw new ERR_TEST_FAILURE(
219+
'cannot set plan more than once',
220+
kTestCodeFailure,
221+
);
222+
}
223+
224+
this.#test.plan = new TestPlan(count);
225+
}
226+
227+
get assert() {
228+
if (this.#assert === undefined) {
229+
const { plan } = this.#test;
230+
const assertions = lazyAssertObject();
231+
const assert = { __proto__: null };
232+
233+
this.#assert = assert;
234+
for (const { 0: method, 1: name } of assertions.entries()) {
235+
assert[name] = (...args) => {
236+
if (plan !== null) {
237+
plan.actual++;
238+
}
239+
return ReflectApply(method, assert, args);
240+
};
241+
}
242+
}
243+
return this.#assert;
244+
}
245+
183246
get mock() {
184247
this.#test.mock ??= new MockTracker();
185248
return this.#test.mock;
@@ -203,6 +266,11 @@ class TestContext {
203266
loc: getCallerLocation(),
204267
};
205268

269+
const { plan } = this.#test;
270+
if (plan !== null) {
271+
plan.actual++;
272+
}
273+
206274
const subtest = this.#test.createSubtest(
207275
// eslint-disable-next-line no-use-before-define
208276
Test, name, options, fn, overrides,
@@ -257,7 +325,7 @@ class Test extends AsyncResource {
257325
super('Test');
258326

259327
let { fn, name, parent } = options;
260-
const { concurrency, loc, only, timeout, todo, skip, signal } = options;
328+
const { concurrency, loc, only, timeout, todo, skip, signal, plan } = options;
261329

262330
if (typeof fn !== 'function') {
263331
fn = noop;
@@ -373,6 +441,8 @@ class Test extends AsyncResource {
373441
this.fn = fn;
374442
this.harness = null; // Configured on the root test by the test harness.
375443
this.mock = null;
444+
this.plan = null;
445+
this.expectedAssertions = plan;
376446
this.cancelled = false;
377447
this.skipped = skip !== undefined && skip !== false;
378448
this.isTodo = todo !== undefined && todo !== false;
@@ -703,6 +773,11 @@ class Test extends AsyncResource {
703773

704774
const hookArgs = this.getRunArgs();
705775
const { args, ctx } = hookArgs;
776+
777+
if (this.plan === null && this.expectedAssertions) {
778+
ctx.plan(this.expectedAssertions);
779+
}
780+
706781
const after = async () => {
707782
if (this.hooks.after.length > 0) {
708783
await this.runHook('after', hookArgs);
@@ -754,7 +829,7 @@ class Test extends AsyncResource {
754829
this.postRun();
755830
return;
756831
}
757-
832+
this.plan?.check();
758833
this.pass();
759834
await afterEach();
760835
await after();
Collapse file
+79Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
'use strict';
2+
const { test } = require('node:test');
3+
const { Readable } = require('node:stream');
4+
5+
test('test planning basic', (t) => {
6+
t.plan(2);
7+
t.assert.ok(true);
8+
t.assert.ok(true);
9+
});
10+
11+
test('less assertions than planned', (t) => {
12+
t.plan(1);
13+
});
14+
15+
test('more assertions than planned', (t) => {
16+
t.plan(1);
17+
t.assert.ok(true);
18+
t.assert.ok(true);
19+
});
20+
21+
test('subtesting', (t) => {
22+
t.plan(1);
23+
t.test('subtest', () => { });
24+
});
25+
26+
test('subtesting correctly', (t) => {
27+
t.plan(2);
28+
t.assert.ok(true);
29+
t.test('subtest', (st) => {
30+
st.plan(1);
31+
st.assert.ok(true);
32+
});
33+
});
34+
35+
test('correctly ignoring subtesting plan', (t) => {
36+
t.plan(1);
37+
t.test('subtest', (st) => {
38+
st.plan(1);
39+
st.assert.ok(true);
40+
});
41+
});
42+
43+
test('failing planning by options', { plan: 1 }, () => {
44+
});
45+
46+
test('not failing planning by options', { plan: 1 }, (t) => {
47+
t.assert.ok(true);
48+
});
49+
50+
test('subtest planning by options', (t) => {
51+
t.test('subtest', { plan: 1 }, (st) => {
52+
st.assert.ok(true);
53+
});
54+
});
55+
56+
test('failing more assertions than planned', (t) => {
57+
t.plan(2);
58+
t.assert.ok(true);
59+
t.assert.ok(true);
60+
t.assert.ok(true);
61+
});
62+
63+
test('planning with streams', (t, done) => {
64+
function* generate() {
65+
yield 'a';
66+
yield 'b';
67+
yield 'c';
68+
}
69+
const expected = ['a', 'b', 'c'];
70+
t.plan(expected.length);
71+
const stream = Readable.from(generate());
72+
stream.on('data', (chunk) => {
73+
t.assert.strictEqual(chunk, expected.shift());
74+
});
75+
76+
stream.on('end', () => {
77+
done();
78+
});
79+
})

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.