Skip to content

Commit 3143566

Browse files
cjihrigRafaelGSS
authored andcommitted
test_runner: add assert.register() API
This commit adds a top level assert.register() API to the test runner. This function allows users to define their own custom assertion functions on the TestContext. Fixes: #52033 PR-URL: #56434 Reviewed-By: Jacob Smith <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Pietro Marchini <[email protected]>
1 parent 332ce54 commit 3143566

File tree

5 files changed

+166
-34
lines changed

5 files changed

+166
-34
lines changed

doc/api/test.md

+23
Original file line numberDiff line numberDiff line change
@@ -1748,6 +1748,29 @@ describe('tests', async () => {
17481748
});
17491749
```
17501750

1751+
## `assert`
1752+
1753+
<!-- YAML
1754+
added: REPLACEME
1755+
-->
1756+
1757+
An object whose methods are used to configure available assertions on the
1758+
`TestContext` objects in the current process. The methods from `node:assert`
1759+
and snapshot testing functions are available by default.
1760+
1761+
It is possible to apply the same configuration to all files by placing common
1762+
configuration code in a module
1763+
preloaded with `--require` or `--import`.
1764+
1765+
### `assert.register(name, fn)`
1766+
1767+
<!-- YAML
1768+
added: REPLACEME
1769+
-->
1770+
1771+
Defines a new assertion function with the provided name and function. If an
1772+
assertion already exists with the same name, it is overwritten.
1773+
17511774
## `snapshot`
17521775

17531776
<!-- YAML

lib/internal/test_runner/assert.js

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use strict';
2+
const {
3+
SafeMap,
4+
} = primordials;
5+
const {
6+
validateFunction,
7+
validateString,
8+
} = require('internal/validators');
9+
const assert = require('assert');
10+
const methodsToCopy = [
11+
'deepEqual',
12+
'deepStrictEqual',
13+
'doesNotMatch',
14+
'doesNotReject',
15+
'doesNotThrow',
16+
'equal',
17+
'fail',
18+
'ifError',
19+
'match',
20+
'notDeepEqual',
21+
'notDeepStrictEqual',
22+
'notEqual',
23+
'notStrictEqual',
24+
'partialDeepStrictEqual',
25+
'rejects',
26+
'strictEqual',
27+
'throws',
28+
];
29+
let assertMap;
30+
31+
function getAssertionMap() {
32+
if (assertMap === undefined) {
33+
assertMap = new SafeMap();
34+
35+
for (let i = 0; i < methodsToCopy.length; i++) {
36+
assertMap.set(methodsToCopy[i], assert[methodsToCopy[i]]);
37+
}
38+
}
39+
40+
return assertMap;
41+
}
42+
43+
function register(name, fn) {
44+
validateString(name, 'name');
45+
validateFunction(fn, 'fn');
46+
const map = getAssertionMap();
47+
map.set(name, fn);
48+
}
49+
50+
module.exports = { getAssertionMap, register };

lib/internal/test_runner/test.js

+18-34
Original file line numberDiff line numberDiff line change
@@ -100,34 +100,15 @@ function lazyFindSourceMap(file) {
100100

101101
function lazyAssertObject(harness) {
102102
if (assertObj === undefined) {
103-
assertObj = new SafeMap();
104-
const assert = require('assert');
105-
const { SnapshotManager } = require('internal/test_runner/snapshot');
106-
const methodsToCopy = [
107-
'deepEqual',
108-
'deepStrictEqual',
109-
'doesNotMatch',
110-
'doesNotReject',
111-
'doesNotThrow',
112-
'equal',
113-
'fail',
114-
'ifError',
115-
'match',
116-
'notDeepEqual',
117-
'notDeepStrictEqual',
118-
'notEqual',
119-
'notStrictEqual',
120-
'partialDeepStrictEqual',
121-
'rejects',
122-
'strictEqual',
123-
'throws',
124-
];
125-
for (let i = 0; i < methodsToCopy.length; i++) {
126-
assertObj.set(methodsToCopy[i], assert[methodsToCopy[i]]);
127-
}
103+
const { getAssertionMap } = require('internal/test_runner/assert');
104+
105+
assertObj = getAssertionMap();
106+
if (!assertObj.has('snapshot')) {
107+
const { SnapshotManager } = require('internal/test_runner/snapshot');
128108

129-
harness.snapshotManager = new SnapshotManager(harness.config.updateSnapshots);
130-
assertObj.set('snapshot', harness.snapshotManager.createAssert());
109+
harness.snapshotManager = new SnapshotManager(harness.config.updateSnapshots);
110+
assertObj.set('snapshot', harness.snapshotManager.createAssert());
111+
}
131112
}
132113
return assertObj;
133114
}
@@ -264,15 +245,18 @@ class TestContext {
264245
};
265246
});
266247

267-
// This is a hack. It allows the innerOk function to collect the stacktrace from the correct starting point.
268-
function ok(...args) {
269-
if (plan !== null) {
270-
plan.actual++;
248+
if (!map.has('ok')) {
249+
// This is a hack. It allows the innerOk function to collect the
250+
// stacktrace from the correct starting point.
251+
function ok(...args) {
252+
if (plan !== null) {
253+
plan.actual++;
254+
}
255+
innerOk(ok, args.length, ...args);
271256
}
272-
innerOk(ok, args.length, ...args);
273-
}
274257

275-
assert.ok = ok;
258+
assert.ok = ok;
259+
}
276260
}
277261
return this.#assert;
278262
}

lib/test.js

+12
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,15 @@ ObjectDefineProperty(module.exports, 'snapshot', {
6161
return lazySnapshot;
6262
},
6363
});
64+
65+
ObjectDefineProperty(module.exports, 'assert', {
66+
__proto__: null,
67+
configurable: true,
68+
enumerable: true,
69+
get() {
70+
const { register } = require('internal/test_runner/assert');
71+
const assert = { __proto__: null, register };
72+
ObjectDefineProperty(module.exports, 'assert', assert);
73+
return assert;
74+
},
75+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'use strict';
2+
require('../common');
3+
const assert = require('node:assert');
4+
const { test, assert: testAssertions } = require('node:test');
5+
6+
testAssertions.register('isOdd', (n) => {
7+
assert.strictEqual(n % 2, 1);
8+
});
9+
10+
testAssertions.register('ok', () => {
11+
return 'ok';
12+
});
13+
14+
testAssertions.register('snapshot', () => {
15+
return 'snapshot';
16+
});
17+
18+
testAssertions.register('deepStrictEqual', () => {
19+
return 'deepStrictEqual';
20+
});
21+
22+
testAssertions.register('context', function() {
23+
return this;
24+
});
25+
26+
test('throws if name is not a string', () => {
27+
assert.throws(() => {
28+
testAssertions.register(5);
29+
}, {
30+
code: 'ERR_INVALID_ARG_TYPE',
31+
message: 'The "name" argument must be of type string. Received type number (5)'
32+
});
33+
});
34+
35+
test('throws if fn is not a function', () => {
36+
assert.throws(() => {
37+
testAssertions.register('foo', 5);
38+
}, {
39+
code: 'ERR_INVALID_ARG_TYPE',
40+
message: 'The "fn" argument must be of type function. Received type number (5)'
41+
});
42+
});
43+
44+
test('invokes a custom assertion as part of the test plan', (t) => {
45+
t.plan(2);
46+
t.assert.isOdd(5);
47+
assert.throws(() => {
48+
t.assert.isOdd(4);
49+
}, {
50+
code: 'ERR_ASSERTION',
51+
message: /Expected values to be strictly equal/
52+
});
53+
});
54+
55+
test('can override existing assertions', (t) => {
56+
assert.strictEqual(t.assert.ok(), 'ok');
57+
assert.strictEqual(t.assert.snapshot(), 'snapshot');
58+
assert.strictEqual(t.assert.deepStrictEqual(), 'deepStrictEqual');
59+
});
60+
61+
test('"this" is set to the TestContext', (t) => {
62+
assert.strictEqual(t.assert.context(), t);
63+
});

0 commit comments

Comments
 (0)