Skip to content

Commit

Permalink
test_runner: before and after each hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
MoLow committed Jul 10, 2022
1 parent a933a75 commit 1dcfe60
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 53 deletions.
12 changes: 12 additions & 0 deletions lib/internal/test_runner/harness.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,20 @@ function runInParentContext(Factory) {
return cb;
}

function afterEach(fn, options) {
const parent = testResources.get(executionAsyncId()) || setup(root);
parent.createBeforeEachHook(fn, options);

}
function beforeEach(fn, options) {
const parent = testResources.get(executionAsyncId()) || setup(root);
parent.createBeforeEachHook(fn, options);
}

module.exports = {
test: FunctionPrototypeBind(test, root),
describe: runInParentContext(Suite),
it: runInParentContext(ItTest),
afterEach,
beforeEach,
};
68 changes: 62 additions & 6 deletions lib/internal/test_runner/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ class TestContext {
this.#test = test;
}

get name() {
return this.#test.name;
}

diagnostic(message) {
this.#test.diagnostic(message);
}
Expand All @@ -69,6 +73,14 @@ class TestContext {

return subtest.start();
}

beforeEach(fn, options) {
this.#test.createBeforeEachHook(fn, options);
}

afterEach(fn, options) {
this.#test.createAfterEachHook(fn, options);
}
}

class Test extends AsyncResource {
Expand Down Expand Up @@ -140,6 +152,8 @@ class Test extends AsyncResource {
this.pendingSubtests = [];
this.readySubtests = new SafeMap();
this.subtests = [];
this.beforeEachHooks = [];
this.afterEachHooks = [];
this.waitingOn = 0;
this.finished = false;
}
Expand Down Expand Up @@ -242,6 +256,18 @@ class Test extends AsyncResource {
return test;
}

createBeforeEachHook(fn, options) {
const hook = new TestHook(fn, options);
ArrayPrototypePush(this.beforeEachHooks, hook);
return hook;
}

createAfterEachHook(fn, options) {
const hook = new TestHook(fn, options);
ArrayPrototypePush(this.afterEachHooks, hook);
return hook;
}

cancel() {
if (this.endTime !== null) {
return;
Expand All @@ -253,6 +279,7 @@ class Test extends AsyncResource {
kCancelledByParent
)
);
this.startTime = this.startTime || this.endTime; // if a test was canceled before it was started, e.g inside a hook
this.cancelled = true;
}

Expand Down Expand Up @@ -309,12 +336,24 @@ class Test extends AsyncResource {
return { ctx, args: [ctx] };
}

async run() {
this.parent.activeSubtests++;
async #runHooks(hooks) {
await ArrayPrototypeReduce(hooks, async (prev, hook) => {
await prev;
await hook.run(this.getRunArgs());
}, PromiseResolve());
}

async run(...runArgs) {
if (this.parent !== null) {
this.parent.activeSubtests++;
}
if (this.parent?.beforeEachHooks.length > 0) {
await this.#runHooks(this.parent.beforeEachHooks);
}
this.startTime = hrtime();

try {
const { args, ctx } = this.getRunArgs();
const { args, ctx } = ReflectApply(this.getRunArgs, this, runArgs);
ArrayPrototypeUnshift(args, this.fn, ctx); // Note that if it's not OK to mutate args, we need to first clone it.

if (this.fn.length === args.length - 1) {
Expand Down Expand Up @@ -347,9 +386,13 @@ class Test extends AsyncResource {
}
}

if (this.parent?.afterEachHooks.length > 0) {
await this.#runHooks(this.parent.afterEachHooks);
}

// Clean up the test. Then, try to report the results and execute any
// tests that were pending due to available concurrency.
this.postRun();
await this.postRun();
}

postRun() {
Expand Down Expand Up @@ -387,7 +430,7 @@ class Test extends AsyncResource {
this.parent.activeSubtests--;
this.parent.addReadySubtest(this);
this.parent.processReadySubtestRange(false);
this.parent.processPendingSubtests();
return this.parent.processPendingSubtests();
}
}

Expand Down Expand Up @@ -447,10 +490,23 @@ class Test extends AsyncResource {
}
}

class TestHook extends Test {
constructor(fn, options) {
if (options === null || typeof options !== 'object') {
options = kEmptyObject;
}
super({ fn, ...options });
}
getRunArgs(testContext) {
return testContext;
}
}

class ItTest extends Test {
constructor(opt) { super(opt); } // eslint-disable-line no-useless-constructor
getRunArgs() {
return { ctx: {}, args: [] };
const ctx = new TestContext(this);
return { ctx, args: [] };
}
}
class Suite extends Test {
Expand Down
4 changes: 3 additions & 1 deletion lib/test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use strict';
const { test, describe, it } = require('internal/test_runner/harness');
const { test, describe, it, afterEach, beforeEach } = require('internal/test_runner/harness');
const { emitExperimentalWarning } = require('internal/util');

emitExperimentalWarning('The test runner');
Expand All @@ -8,3 +8,5 @@ module.exports = test;
module.exports.test = test;
module.exports.describe = describe;
module.exports.it = it;
module.exports.afterEach = afterEach;
module.exports.beforeEach = beforeEach;
13 changes: 0 additions & 13 deletions test/message/test_runner_desctibe_it.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,19 +219,6 @@ it('callback fail', (done) => {
});
});

it('sync t is this in test', function() {
assert.deepStrictEqual(this, {});
});

it('async t is this in test', async function() {
assert.deepStrictEqual(this, {});
});

it('callback t is this in test', function(done) {
assert.deepStrictEqual(this, {});
done();
});

it('callback also returns a Promise', async (done) => {
throw new Error('thrown from callback also returns a Promise');
});
Expand Down
65 changes: 34 additions & 31 deletions test/message/test_runner_desctibe_it.out
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ not ok 8 - sync throw fail
*
*
*
*
*
*
...
# Subtest: async skip pass
ok 9 - async skip pass # SKIP
Expand All @@ -100,6 +103,9 @@ not ok 11 - async throw fail
*
*
*
*
*
*
...
# Subtest: async assertion fail
not ok 12 - async assertion fail
Expand All @@ -108,9 +114,9 @@ not ok 12 - async assertion fail
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly equal:

true !== false

code: 'ERR_ASSERTION'
stack: |-
*
Expand All @@ -120,6 +126,9 @@ not ok 12 - async assertion fail
*
*
*
*
*
*
...
# Subtest: resolve pass
ok 13 - resolve pass
Expand All @@ -141,6 +150,9 @@ not ok 14 - reject fail
*
*
*
*
*
*
...
# Subtest: unhandled rejection - passes but warns
ok 15 - unhandled rejection - passes but warns
Expand Down Expand Up @@ -277,6 +289,9 @@ not ok 27 - sync skip option is false fail
*
*
*
*
*
*
...
# Subtest: <anonymous>
ok 28 - <anonymous>
Expand Down Expand Up @@ -349,31 +364,16 @@ not ok 40 - callback fail
*
*
...
# Subtest: sync t is this in test
ok 41 - sync t is this in test
---
duration_ms: *
...
# Subtest: async t is this in test
ok 42 - async t is this in test
---
duration_ms: *
...
# Subtest: callback t is this in test
ok 43 - callback t is this in test
---
duration_ms: *
...
# Subtest: callback also returns a Promise
not ok 44 - callback also returns a Promise
not ok 41 - callback also returns a Promise
---
duration_ms: *
failureType: 'callbackAndPromisePresent'
error: 'passed a callback but also returned a Promise'
code: 'ERR_TEST_FAILURE'
...
# Subtest: callback throw
not ok 45 - callback throw
not ok 42 - callback throw
---
duration_ms: *
failureType: 'testCodeFailure'
Expand All @@ -387,9 +387,12 @@ not ok 45 - callback throw
*
*
*
*
*
*
...
# Subtest: callback called twice
not ok 46 - callback called twice
not ok 43 - callback called twice
---
duration_ms: *
failureType: 'multipleCallbackInvocations'
Expand All @@ -400,12 +403,12 @@ not ok 46 - callback called twice
*
...
# Subtest: callback called twice in different ticks
ok 47 - callback called twice in different ticks
ok 44 - callback called twice in different ticks
---
duration_ms: *
...
# Subtest: callback called twice in future tick
not ok 48 - callback called twice in future tick
not ok 45 - callback called twice in future tick
---
duration_ms: *
failureType: 'uncaughtException'
Expand All @@ -415,7 +418,7 @@ not ok 48 - callback called twice in future tick
*
...
# Subtest: callback async throw
not ok 49 - callback async throw
not ok 46 - callback async throw
---
duration_ms: *
failureType: 'uncaughtException'
Expand All @@ -425,20 +428,20 @@ not ok 49 - callback async throw
*
...
# Subtest: callback async throw after done
ok 50 - callback async throw after done
ok 47 - callback async throw after done
---
duration_ms: *
...
# Subtest: custom inspect symbol fail
not ok 51 - custom inspect symbol fail
not ok 48 - custom inspect symbol fail
---
duration_ms: *
failureType: 'testCodeFailure'
error: 'customized'
code: 'ERR_TEST_FAILURE'
...
# Subtest: custom inspect symbol that throws fail
not ok 52 - custom inspect symbol that throws fail
not ok 49 - custom inspect symbol that throws fail
---
duration_ms: *
failureType: 'testCodeFailure'
Expand Down Expand Up @@ -482,15 +485,15 @@ not ok 52 - custom inspect symbol that throws fail
*
...
1..2
not ok 53 - subtest sync throw fails
not ok 50 - subtest sync throw fails
---
duration_ms: *
failureType: 'subtestsFailed'
error: '2 subtests failed'
code: 'ERR_TEST_FAILURE'
...
# Subtest: invalid subtest fail
not ok 54 - invalid subtest fail
not ok 51 - invalid subtest fail
---
duration_ms: *
failureType: 'parentAlreadyFinished'
Expand All @@ -499,15 +502,15 @@ not ok 54 - invalid subtest fail
stack: |-
*
...
1..54
1..51
# Warning: Test "unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
# Warning: Test "async unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
# Warning: Test "immediate throw - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from immediate throw fail" and would have caused the test to fail, but instead triggered an uncaughtException event.
# Warning: Test "immediate reject - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
# Warning: Test "callback called twice in different ticks" generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event.
# Warning: Test "callback async throw after done" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event.
# tests 54
# pass 23
# tests 51
# pass 20
# fail 17
# cancelled 0
# skipped 9
Expand Down
Loading

0 comments on commit 1dcfe60

Please sign in to comment.