Skip to content

Commit 4f983d5

Browse files
committed
test_runner: add before/after/each hooks
1 parent 2fd4c01 commit 4f983d5

File tree

5 files changed

+159
-15
lines changed

5 files changed

+159
-15
lines changed

lib/internal/test_runner/harness.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,19 @@ function runInParentContext(Factory) {
170170
return cb;
171171
}
172172

173+
function hook(hook) {
174+
return (fn, options) => {
175+
const parent = testResources.get(executionAsyncId()) || setup(root);
176+
parent.createHook(hook, fn, options);
177+
};
178+
}
179+
173180
module.exports = {
174181
test: FunctionPrototypeBind(test, root),
175182
describe: runInParentContext(Suite),
176183
it: runInParentContext(ItTest),
184+
before: hook('before'),
185+
after: hook('after'),
186+
beforeEach: hook('beforeEach'),
187+
afterEach: hook('afterEach'),
177188
};

lib/internal/test_runner/test.js

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
'use strict';
22
const {
33
ArrayPrototypePush,
4+
ArrayPrototypeReduce,
45
ArrayPrototypeShift,
6+
ArrayPrototypeSlice,
57
ArrayPrototypeUnshift,
68
FunctionPrototype,
79
Number,
10+
ObjectSeal,
811
PromisePrototypeThen,
912
PromiseResolve,
1013
ReflectApply,
@@ -31,7 +34,7 @@ const {
3134
kEmptyObject,
3235
} = require('internal/util');
3336
const { isPromise } = require('internal/util/types');
34-
const { isUint32, validateAbortSignal } = require('internal/validators');
37+
const { isUint32, validateAbortSignal, validateOneOf } = require('internal/validators');
3538
const { setTimeout } = require('timers/promises');
3639
const { cpus } = require('os');
3740
const { bigint: hrtime } = process.hrtime;
@@ -50,6 +53,8 @@ const testOnlyFlag = !isTestRunner && getOptionValue('--test-only');
5053
const rootConcurrency = isTestRunner ? cpus().length : 1;
5154

5255
const kShouldAbort = Symbol('kShouldAbort');
56+
const kRunHook = Symbol('kRunHook');
57+
const kHookNames = ObjectSeal(['before', 'after', 'beforeEach', 'afterEach']);
5358

5459

5560
function stopTest(timeout, signal) {
@@ -75,6 +80,10 @@ class TestContext {
7580
return this.#test.signal;
7681
}
7782

83+
get name() {
84+
return this.#test.name;
85+
}
86+
7887
diagnostic(message) {
7988
this.#test.diagnostic(message);
8089
}
@@ -97,6 +106,14 @@ class TestContext {
97106

98107
return subtest.start();
99108
}
109+
110+
beforeEach(fn, options) {
111+
this.#test.createHook('beforeEach', fn, options);
112+
}
113+
114+
afterEach(fn, options) {
115+
this.#test.createHook('afterEach', fn, options);
116+
}
100117
}
101118

102119
class Test extends AsyncResource {
@@ -185,6 +202,12 @@ class Test extends AsyncResource {
185202
this.pendingSubtests = [];
186203
this.readySubtests = new SafeMap();
187204
this.subtests = [];
205+
this.hooks = {
206+
before: [],
207+
after: [],
208+
beforeEach: [],
209+
afterEach: [],
210+
};
188211
this.waitingOn = 0;
189212
this.finished = false;
190213
}
@@ -303,10 +326,19 @@ class Test extends AsyncResource {
303326
kCancelledByParent
304327
)
305328
);
329+
this.startTime = this.startTime || this.endTime; // If a test was canceled before it was started, e.g inside a hook
306330
this.cancelled = true;
307331
this.#abortController.abort();
308332
}
309333

334+
createHook(name, fn, options) {
335+
validateOneOf(name, 'hook name', kHookNames);
336+
// eslint-disable-next-line no-use-before-define
337+
const hook = new TestHook(fn, options);
338+
ArrayPrototypePush(this.hooks[name], hook);
339+
return hook;
340+
}
341+
310342
fail(err) {
311343
if (this.error !== null) {
312344
return;
@@ -370,26 +402,41 @@ class Test extends AsyncResource {
370402
return { ctx, args: [ctx] };
371403
}
372404

405+
async [kRunHook](hook, args) {
406+
validateOneOf(hook, 'hook name', kHookNames);
407+
await ArrayPrototypeReduce(this.hooks[hook], async (prev, hook) => {
408+
await prev;
409+
await hook.run(args);
410+
}, PromiseResolve());
411+
}
412+
373413
async run() {
374-
this.parent.activeSubtests++;
414+
if (this.parent !== null) {
415+
this.parent.activeSubtests++;
416+
}
375417
this.startTime = hrtime();
376418

377419
if (this[kShouldAbort]()) {
378420
this.postRun();
379421
return;
380422
}
381423

424+
const { args, ctx } = this.getRunArgs();
425+
if (this.parent?.hooks.beforeEach.length > 0) {
426+
await this.parent[kRunHook]('beforeEach', { args, ctx });
427+
}
428+
382429
try {
383430
const stopPromise = stopTest(this.timeout, this.signal);
384-
const { args, ctx } = this.getRunArgs();
385-
ArrayPrototypeUnshift(args, this.fn, ctx); // Note that if it's not OK to mutate args, we need to first clone it.
431+
const runArgs = ArrayPrototypeSlice(args);
432+
ArrayPrototypeUnshift(runArgs, this.fn, ctx);
386433

387-
if (this.fn.length === args.length - 1) {
434+
if (this.fn.length === runArgs.length - 1) {
388435
// This test is using legacy Node.js error first callbacks.
389436
const { promise, cb } = createDeferredCallback();
390437

391-
ArrayPrototypePush(args, cb);
392-
const ret = ReflectApply(this.runInAsyncScope, this, args);
438+
ArrayPrototypePush(runArgs, cb);
439+
const ret = ReflectApply(this.runInAsyncScope, this, runArgs);
393440

394441
if (isPromise(ret)) {
395442
this.fail(new ERR_TEST_FAILURE(
@@ -402,7 +449,7 @@ class Test extends AsyncResource {
402449
}
403450
} else {
404451
// This test is synchronous or using Promises.
405-
const promise = ReflectApply(this.runInAsyncScope, this, args);
452+
const promise = ReflectApply(this.runInAsyncScope, this, runArgs);
406453
await SafePromiseRace([PromiseResolve(promise), stopPromise]);
407454
}
408455

@@ -420,6 +467,10 @@ class Test extends AsyncResource {
420467
}
421468
}
422469

470+
if (this.parent?.hooks.afterEach.length > 0) {
471+
await this.parent[kRunHook]('afterEach', { args, ctx });
472+
}
473+
423474
// Clean up the test. Then, try to report the results and execute any
424475
// tests that were pending due to available concurrency.
425476
this.postRun();
@@ -523,26 +574,47 @@ class Test extends AsyncResource {
523574
}
524575
}
525576

577+
class TestHook extends Test {
578+
#args;
579+
constructor(fn, options) {
580+
if (options === null || typeof options !== 'object') {
581+
options = kEmptyObject;
582+
}
583+
super({ __proto__: null, fn, ...options });
584+
}
585+
run(args) {
586+
this.#args = args;
587+
return super.run();
588+
}
589+
getRunArgs() {
590+
return this.#args;
591+
}
592+
}
593+
526594
class ItTest extends Test {
527595
constructor(opt) { super(opt); } // eslint-disable-line no-useless-constructor
528596
getRunArgs() {
529-
return { ctx: { signal: this.signal }, args: [] };
597+
return { ctx: { signal: this.signal, name: this.name }, args: [] };
530598
}
531599
}
532600
class Suite extends Test {
533601
constructor(options) {
534602
super(options);
535603

536604
try {
537-
const context = { signal: this.signal };
538-
this.buildSuite = this.runInAsyncScope(this.fn, context, [context]);
605+
const { ctx, args } = this.getRunArgs();
606+
this.buildSuite = this.runInAsyncScope(this.fn, ctx, args);
539607
} catch (err) {
540608
this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure));
541609
}
542610
this.fn = () => {};
543611
this.buildPhaseFinished = true;
544612
}
545613

614+
getRunArgs() {
615+
return { ctx: { signal: this.signal, name: this.name }, args: [] };
616+
}
617+
546618
start() {
547619
return this.run();
548620
}
@@ -562,11 +634,16 @@ class Suite extends Test {
562634
return;
563635
}
564636

637+
638+
const hookArgs = this.getRunArgs();
639+
await this[kRunHook]('before', hookArgs);
565640
const stopPromise = stopTest(this.timeout, this.signal);
566641
const subtests = this.skipped || this.error ? [] : this.subtests;
567642
const promise = SafePromiseAll(subtests, (subtests) => subtests.start());
568643

569644
await SafePromiseRace([promise, stopPromise]);
645+
await this[kRunHook]('after', hookArgs);
646+
570647
this.pass();
571648
this.postRun();
572649
}

lib/test.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use strict';
2-
const { test, describe, it } = require('internal/test_runner/harness');
2+
const { test, describe, it, before, after, beforeEach, afterEach } = require('internal/test_runner/harness');
33
const { emitExperimentalWarning } = require('internal/util');
44

55
emitExperimentalWarning('The test runner');
@@ -8,3 +8,7 @@ module.exports = test;
88
module.exports.test = test;
99
module.exports.describe = describe;
1010
module.exports.it = it;
11+
module.exports.before = before;
12+
module.exports.after = after;
13+
module.exports.beforeEach = beforeEach;
14+
module.exports.afterEach = afterEach;

test/message/test_runner_describe_it.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,15 +225,15 @@ it('callback fail', (done) => {
225225
});
226226

227227
it('sync t is this in test', function() {
228-
assert.deepStrictEqual(this, { signal: this.signal });
228+
assert.deepStrictEqual(this, { signal: this.signal, name: this.name });
229229
});
230230

231231
it('async t is this in test', async function() {
232-
assert.deepStrictEqual(this, { signal: this.signal });
232+
assert.deepStrictEqual(this, { signal: this.signal, name: this.name });
233233
});
234234

235235
it('callback t is this in test', function(done) {
236-
assert.deepStrictEqual(this, { signal: this.signal });
236+
assert.deepStrictEqual(this, { signal: this.signal, name: this.name });
237237
done();
238238
});
239239

test/parallel/test-runner-hooks.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const { describe, it, before, after, beforeEach, afterEach, test } = require('node:test');
5+
6+
const testArr = [];
7+
const afterTest = common.mustCall(() => {
8+
assert.deepStrictEqual(testArr, [
9+
'beforeEach 1', '1', 'afterEach 1',
10+
'beforeEach 2', '2', 'afterEach 2',
11+
'beforeEach 3', '3', 'afterEach 3',
12+
]);
13+
});
14+
test('test', async (t) => {
15+
t.beforeEach((t) => testArr.push('beforeEach ' + t.name));
16+
t.afterEach((t) => testArr.push('afterEach ' + t.name));
17+
18+
await t.test('1', () => testArr.push('1'));
19+
await t.test('2', () => testArr.push('2'));
20+
await t.test('3', () => testArr.push('3'));
21+
}).then(afterTest);
22+
23+
24+
const describeArr = [];
25+
const afterDescribe = common.mustCall(() => {
26+
assert.deepStrictEqual(describeArr, [
27+
'before describe hooks',
28+
'beforeEach 1', '1', 'afterEach 1',
29+
'beforeEach 2', '2', 'afterEach 2',
30+
'beforeEach 3', '3', 'afterEach 3',
31+
'after describe hooks',
32+
]);
33+
});
34+
describe('describe hooks', () => {
35+
before(function() {
36+
describeArr.push('before ' + this.name);
37+
});
38+
after(function() {
39+
describeArr.push('after ' + this.name);
40+
afterDescribe();
41+
});
42+
beforeEach(function() {
43+
describeArr.push('beforeEach ' + this.name);
44+
});
45+
afterEach(function() {
46+
describeArr.push('afterEach ' + this.name);
47+
});
48+
49+
it('1', () => describeArr.push('1'));
50+
it('2', () => describeArr.push('2'));
51+
it('3', () => describeArr.push('3'));
52+
});

0 commit comments

Comments
 (0)