Skip to content

Commit e275ae8

Browse files
committed
test_runner: add before/after/each hooks
1 parent 7ef069e commit e275ae8

File tree

8 files changed

+436
-39
lines changed

8 files changed

+436
-39
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: 118 additions & 29 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,
@@ -20,18 +23,17 @@ const {
2023
codes: {
2124
ERR_TEST_FAILURE,
2225
},
23-
kIsNodeError,
2426
AbortError,
2527
} = require('internal/errors');
2628
const { getOptionValue } = require('internal/options');
2729
const { TapStream } = require('internal/test_runner/tap_stream');
28-
const { createDeferredCallback } = require('internal/test_runner/utils');
30+
const { createDeferredCallback, isTetFailureError } = require('internal/test_runner/utils');
2931
const {
3032
createDeferredPromise,
3133
kEmptyObject,
3234
} = require('internal/util');
3335
const { isPromise } = require('internal/util/types');
34-
const { isUint32, validateAbortSignal } = require('internal/validators');
36+
const { isUint32, validateAbortSignal, validateOneOf } = require('internal/validators');
3537
const { setTimeout } = require('timers/promises');
3638
const { cpus } = require('os');
3739
const { bigint: hrtime } = process.hrtime;
@@ -41,6 +43,7 @@ const kParentAlreadyFinished = 'parentAlreadyFinished';
4143
const kSubtestsFailed = 'subtestsFailed';
4244
const kTestCodeFailure = 'testCodeFailure';
4345
const kTestTimeoutFailure = 'testTimeoutFailure';
46+
const kHookFailure = 'hookFailed';
4447
const kDefaultIndent = ' ';
4548
const kDefaultTimeout = null;
4649
const noop = FunctionPrototype;
@@ -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,8 +402,27 @@ class Test extends AsyncResource {
370402
return { ctx, args: [ctx] };
371403
}
372404

405+
async [kRunHook](hook, args) {
406+
validateOneOf(hook, 'hook name', kHookNames);
407+
try {
408+
await ArrayPrototypeReduce(this.hooks[hook], async (prev, hook) => {
409+
await prev;
410+
await hook.run(args);
411+
if (hook.error) {
412+
throw hook.error;
413+
}
414+
}, PromiseResolve());
415+
} catch (err) {
416+
const error = new ERR_TEST_FAILURE(`failed running ${hook} hook`, kHookFailure);
417+
error.cause = isTetFailureError(err) ? err.cause : err;
418+
throw error;
419+
}
420+
}
421+
373422
async run() {
374-
this.parent.activeSubtests++;
423+
if (this.parent !== null) {
424+
this.parent.activeSubtests++;
425+
}
375426
this.startTime = hrtime();
376427

377428
if (this[kShouldAbort]()) {
@@ -380,16 +431,20 @@ class Test extends AsyncResource {
380431
}
381432

382433
try {
383-
const stopPromise = stopTest(this.timeout, this.signal);
384434
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.
435+
if (this.parent?.hooks.beforeEach.length > 0) {
436+
await this.parent[kRunHook]('beforeEach', { args, ctx });
437+
}
438+
const stopPromise = stopTest(this.timeout, this.signal);
439+
const runArgs = ArrayPrototypeSlice(args);
440+
ArrayPrototypeUnshift(runArgs, this.fn, ctx);
386441

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

391-
ArrayPrototypePush(args, cb);
392-
const ret = ReflectApply(this.runInAsyncScope, this, args);
446+
ArrayPrototypePush(runArgs, cb);
447+
const ret = ReflectApply(this.runInAsyncScope, this, runArgs);
393448

394449
if (isPromise(ret)) {
395450
this.fail(new ERR_TEST_FAILURE(
@@ -402,7 +457,7 @@ class Test extends AsyncResource {
402457
}
403458
} else {
404459
// This test is synchronous or using Promises.
405-
const promise = ReflectApply(this.runInAsyncScope, this, args);
460+
const promise = ReflectApply(this.runInAsyncScope, this, runArgs);
406461
await SafePromiseRace([PromiseResolve(promise), stopPromise]);
407462
}
408463

@@ -411,9 +466,13 @@ class Test extends AsyncResource {
411466
return;
412467
}
413468

469+
if (this.parent?.hooks.afterEach.length > 0) {
470+
await this.parent[kRunHook]('afterEach', { args, ctx });
471+
}
472+
414473
this.pass();
415474
} catch (err) {
416-
if (err?.code === 'ERR_TEST_FAILURE' && kIsNodeError in err) {
475+
if (isTetFailureError(err)) {
417476
this.fail(err);
418477
} else {
419478
this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure));
@@ -523,51 +582,81 @@ class Test extends AsyncResource {
523582
}
524583
}
525584

585+
class TestHook extends Test {
586+
#args;
587+
constructor(fn, options) {
588+
if (options === null || typeof options !== 'object') {
589+
options = kEmptyObject;
590+
}
591+
super({ __proto__: null, fn, ...options });
592+
}
593+
run(args) {
594+
this.#args = args;
595+
return super.run();
596+
}
597+
getRunArgs() {
598+
return this.#args;
599+
}
600+
}
601+
526602
class ItTest extends Test {
527603
constructor(opt) { super(opt); } // eslint-disable-line no-useless-constructor
528604
getRunArgs() {
529-
return { ctx: { signal: this.signal }, args: [] };
605+
return { ctx: { signal: this.signal, name: this.name }, args: [] };
530606
}
531607
}
532608
class Suite extends Test {
533609
constructor(options) {
534610
super(options);
535611

536612
try {
537-
const context = { signal: this.signal };
538-
this.buildSuite = this.runInAsyncScope(this.fn, context, [context]);
613+
const { ctx, args } = this.getRunArgs();
614+
this.buildSuite = this.runInAsyncScope(this.fn, ctx, args);
539615
} catch (err) {
540616
this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure));
541617
}
542618
this.fn = () => {};
543619
this.buildPhaseFinished = true;
544620
}
545621

622+
getRunArgs() {
623+
return { ctx: { signal: this.signal, name: this.name }, args: [] };
624+
}
625+
546626
start() {
547627
return this.run();
548628
}
549629

550630
async run() {
551631
try {
552632
await this.buildSuite;
553-
} catch (err) {
554-
this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure));
555-
}
556-
this.parent.activeSubtests++;
557-
this.startTime = hrtime();
633+
this.parent.activeSubtests++;
634+
this.startTime = hrtime();
558635

559-
if (this[kShouldAbort]()) {
560-
this.subtests = [];
561-
this.postRun();
562-
return;
563-
}
636+
if (this[kShouldAbort]()) {
637+
this.subtests = [];
638+
this.postRun();
639+
return;
640+
}
641+
642+
643+
const hookArgs = this.getRunArgs();
644+
await this[kRunHook]('before', hookArgs);
645+
const stopPromise = stopTest(this.timeout, this.signal);
646+
const subtests = this.skipped || this.error ? [] : this.subtests;
647+
const promise = SafePromiseAll(subtests, (subtests) => subtests.start());
564648

565-
const stopPromise = stopTest(this.timeout, this.signal);
566-
const subtests = this.skipped || this.error ? [] : this.subtests;
567-
const promise = SafePromiseAll(subtests, (subtests) => subtests.start());
649+
await SafePromiseRace([promise, stopPromise]);
650+
await this[kRunHook]('after', hookArgs);
651+
this.pass();
652+
} catch (err) {
653+
if (isTetFailureError(err)) {
654+
this.fail(err);
655+
} else {
656+
this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure));
657+
}
658+
}
568659

569-
await SafePromiseRace([promise, stopPromise]);
570-
this.pass();
571660
this.postRun();
572661
}
573662
}

lib/internal/test_runner/utils.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const {
66
codes: {
77
ERR_TEST_FAILURE,
88
},
9+
kIsNodeError,
910
} = require('internal/errors');
1011

1112
const kMultipleCallbackInvocations = 'multipleCallbackInvocations';
@@ -49,8 +50,13 @@ function createDeferredCallback() {
4950
return { promise, cb };
5051
}
5152

53+
function isTetFailureError(err) {
54+
return err?.code === 'ERR_TEST_FAILURE' && kIsNodeError in err;
55+
}
56+
5257
module.exports = {
5358
createDeferredCallback,
5459
doesPathMatchFilter,
5560
isSupportedFileType,
61+
isTetFailureError,
5662
};

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

0 commit comments

Comments
 (0)