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 <jacob@frende.me> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Pietro Marchini <pietro.marchini94@gmail.com>
1 parent 332ce54 commit 3143566

5 files changed

Lines changed: 166 additions & 34 deletions

File tree

‎doc/api/test.md‎

Lines changed: 23 additions & 0 deletions
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‎

Lines changed: 50 additions & 0 deletions
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+
constassert=require('assert');
10+
constmethodsToCopy=[
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+
letassertMap;
30+
31+
functiongetAssertionMap(){
32+
if(assertMap===undefined){
33+
assertMap=newSafeMap();
34+
35+
for(leti=0;i<methodsToCopy.length;i++){
36+
assertMap.set(methodsToCopy[i],assert[methodsToCopy[i]]);
37+
}
38+
}
39+
40+
returnassertMap;
41+
}
42+
43+
functionregister(name,fn){
44+
validateString(name,'name');
45+
validateFunction(fn,'fn');
46+
constmap=getAssertionMap();
47+
map.set(name,fn);
48+
}
49+
50+
module.exports={ getAssertionMap, register };

‎lib/internal/test_runner/test.js‎

Lines changed: 18 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -100,34 +100,15 @@ function lazyFindSourceMap(file) {
100100

101101
functionlazyAssertObject(harness){
102102
if(assertObj===undefined){
103-
assertObj=newSafeMap();
104-
constassert=require('assert');
105-
const{ SnapshotManager }=require('internal/test_runner/snapshot');
106-
constmethodsToCopy=[
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(leti=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=newSnapshotManager(harness.config.updateSnapshots);
130-
assertObj.set('snapshot',harness.snapshotManager.createAssert());
109+
harness.snapshotManager=newSnapshotManager(harness.config.updateSnapshots);
110+
assertObj.set('snapshot',harness.snapshotManager.createAssert());
111+
}
131112
}
132113
returnassertObj;
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-
functionok(...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+
functionok(...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
returnthis.#assert;
278262
}

‎lib/test.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,15 @@ ObjectDefineProperty(module.exports, 'snapshot', {
6161
returnlazySnapshot;
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+
constassert={__proto__: null, register };
72+
ObjectDefineProperty(module.exports,'assert',assert);
73+
returnassert;
74+
},
75+
});
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'use strict';
2+
require('../common');
3+
constassert=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+
returnthis;
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: /Expectedvaluestobestrictlyequal/
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)