| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1364,6 +1364,10 @@ changes: | |||
| 1364 | 1364 | * `timeout` {number} A number of milliseconds the test will fail after. | |
| 1365 | 1365 | If unspecified, subtests inherit this value from their parent. | |
| 1366 | 1366 | **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`. | ||
| 1367 | 1371 | * `fn` {Function|AsyncFunction} The function under test. The first argument | |
| 1368 | 1372 | to this function is a [`TestContext`][] object. If the test uses callbacks, | |
| 1369 | 1373 | the callback function is passed as the second argument. **Default:** A no-op | |
@@ -2965,6 +2969,54 @@ added: | |||
| 2965 | 2969 | ||
| 2966 | 2970 | The name of the test. | |
| 2967 | 2971 | ||
| 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 | + | ||
| 2968 | 3020 | ### `context.runOnly(shouldRunOnlyTests)` | |
| 2969 | 3021 | ||
| 2970 | 3022 | <!-- YAML | |
@@ -3095,6 +3147,10 @@ changes: | |||
| 3095 | 3147 | * `timeout` {number} A number of milliseconds the test will fail after. | |
| 3096 | 3148 | If unspecified, subtests inherit this value from their parent. | |
| 3097 | 3149 | **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`. | ||
| 3098 | 3154 | * `fn` {Function|AsyncFunction} The function under test. The first argument | |
| 3099 | 3155 | to this function is a [`TestContext`][] object. If the test uses callbacks, | |
| 3100 | 3156 | 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. | |||
| 3108 | 3164 | test('top level test', async (t) => { | |
| 3109 | 3165 | await t.test( | |
| 3110 | 3166 | 'This is a subtest', | |
| 3111 | - { only: false, skip: false, concurrency: 1, todo: false }, | ||
| 3167 | + { only: false, skip: false, concurrency: 1, todo: false, plan: 4 }, | ||
| 3112 | 3168 | (t) => { | |
| 3113 | 3169 | assert.ok('some relevant assertion here'); | |
| 3114 | 3170 | }, | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -462,6 +462,7 @@ function run(options = kEmptyObject) { | |||
| 462 | 462 | watch, | |
| 463 | 463 | setup, | |
| 464 | 464 | only, | |
| 465 | + plan, | ||
| 465 | 466 | } = options; | |
| 466 | 467 | ||
| 467 | 468 | if (files != null) { | |
@@ -534,7 +535,7 @@ function run(options = kEmptyObject) { | |||
| 534 | 535 | }); | |
| 535 | 536 | } | |
| 536 | 537 | ||
| 537 | - const root = createTestTree({ __proto__: null, concurrency, timeout, signal }); | ||
| 538 | + const root = createTestTree({ __proto__: null, concurrency, timeout, signal, plan }); | ||
| 538 | 539 | root.harness.shouldColorizeTestFiles ||= shouldColorizeTestFiles(root); | |
| 539 | 540 | ||
| 540 | 541 | if (process.env.NODE_TEST_CONTEXT !== undefined) { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -12,6 +12,7 @@ const { | |||
| 12 | 12 | MathMax, | |
| 13 | 13 | Number, | |
| 14 | 14 | ObjectDefineProperty, | |
| 15 | + ObjectEntries, | ||
| 15 | 16 | ObjectSeal, | |
| 16 | 17 | PromisePrototypeThen, | |
| 17 | 18 | PromiseResolve, | |
@@ -88,6 +89,7 @@ const { | |||
| 88 | 89 | testOnlyFlag, | |
| 89 | 90 | } = parseCommandLine(); | |
| 90 | 91 | let kResistStopPropagation; | |
| 92 | + let assertObj; | ||
| 91 | 93 | let findSourceMap; | |
| 92 | 94 | let noopTestStream; | |
| 93 | 95 | ||
@@ -101,6 +103,19 @@ function lazyFindSourceMap(file) { | |||
| 101 | 103 | return findSourceMap(file); | |
| 102 | 104 | } | |
| 103 | 105 | ||
| 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 | + | ||
| 104 | 119 | function stopTest(timeout, signal) { | |
| 105 | 120 | const deferred = createDeferredPromise(); | |
| 106 | 121 | const abortListener = addAbortListener(signal, deferred.resolve); | |
@@ -153,7 +168,25 @@ function testMatchesPattern(test, patterns) { | |||
| 153 | 168 | ); | |
| 154 | 169 | } | |
| 155 | 170 | ||
| 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 | + | ||
| 156 | 188 | class TestContext { | |
| 189 | + #assert; | ||
| 157 | 190 | #test; | |
| 158 | 191 | ||
| 159 | 192 | constructor(test) { | |
@@ -180,6 +213,36 @@ class TestContext { | |||
| 180 | 213 | this.#test.diagnostic(message); | |
| 181 | 214 | } | |
| 182 | 215 | ||
| 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 | + | ||
| 183 | 246 | get mock() { | |
| 184 | 247 | this.#test.mock ??= new MockTracker(); | |
| 185 | 248 | return this.#test.mock; | |
@@ -203,6 +266,11 @@ class TestContext { | |||
| 203 | 266 | loc: getCallerLocation(), | |
| 204 | 267 | }; | |
| 205 | 268 | ||
| 269 | + const { plan } = this.#test; | ||
| 270 | + if (plan !== null) { | ||
| 271 | + plan.actual++; | ||
| 272 | + } | ||
| 273 | + | ||
| 206 | 274 | const subtest = this.#test.createSubtest( | |
| 207 | 275 | // eslint-disable-next-line no-use-before-define | |
| 208 | 276 | Test, name, options, fn, overrides, | |
@@ -257,7 +325,7 @@ class Test extends AsyncResource { | |||
| 257 | 325 | super('Test'); | |
| 258 | 326 | ||
| 259 | 327 | 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; | ||
| 261 | 329 | ||
| 262 | 330 | if (typeof fn !== 'function') { | |
| 263 | 331 | fn = noop; | |
@@ -373,6 +441,8 @@ class Test extends AsyncResource { | |||
| 373 | 441 | this.fn = fn; | |
| 374 | 442 | this.harness = null; // Configured on the root test by the test harness. | |
| 375 | 443 | this.mock = null; | |
| 444 | + this.plan = null; | ||
| 445 | + this.expectedAssertions = plan; | ||
| 376 | 446 | this.cancelled = false; | |
| 377 | 447 | this.skipped = skip !== undefined && skip !== false; | |
| 378 | 448 | this.isTodo = todo !== undefined && todo !== false; | |
@@ -703,6 +773,11 @@ class Test extends AsyncResource { | |||
| 703 | 773 | ||
| 704 | 774 | const hookArgs = this.getRunArgs(); | |
| 705 | 775 | const { args, ctx } = hookArgs; | |
| 776 | + | ||
| 777 | + if (this.plan === null && this.expectedAssertions) { | ||
| 778 | + ctx.plan(this.expectedAssertions); | ||
| 779 | + } | ||
| 780 | + | ||
| 706 | 781 | const after = async () => { | |
| 707 | 782 | if (this.hooks.after.length > 0) { | |
| 708 | 783 | await this.runHook('after', hookArgs); | |
@@ -754,7 +829,7 @@ class Test extends AsyncResource { | |||
| 754 | 829 | this.postRun(); | |
| 755 | 830 | return; | |
| 756 | 831 | } | |
| 757 | - | ||
| 832 | + this.plan?.check(); | ||
| 758 | 833 | this.pass(); | |
| 759 | 834 | await afterEach(); | |
| 760 | 835 | await after(); | |
| Original file line number | Diff line number | Diff 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 | + }) | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments