Skip to content

Commit bae6111

Browse files
JakobJingleheimervespa7
authored andcommitted
test_runner: support expecting a test-case to fail
Co-Authored-By: Alejandro Espa <98526766+vespa7@users.noreply.github.com> PR-URL: #60669 Reviewed-By: Aviv Keller <me@aviv.sh> Reviewed-By: Pietro Marchini <pietro.marchini94@gmail.com> Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Jordan Harband <ljharb@gmail.com>
1 parent 3b27cc8 commit bae6111

22 files changed

+806
-493
lines changed

doc/api/test.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,57 @@ test('todo() method with message', (t) => {
224224
});
225225
```
226226

227+
## Expecting tests to fail
228+
229+
<!-- YAML
230+
added:
231+
- REPLACEME
232+
-->
233+
234+
This flips the pass/fail reporting for a specific test or suite: A flagged test/test-case must throw
235+
in order to "pass"; a test/test-case that does not throw, fails.
236+
237+
In the following, `doTheThing()` returns _currently_ `false` (`false` does not equal `true`, causing
238+
`strictEqual` to throw, so the test-case passes).
239+
240+
```js
241+
it.expectFailure('should do the thing', () => {
242+
assert.strictEqual(doTheThing(), true);
243+
});
244+
245+
it('should do the thing', { expectFailure: true }, () => {
246+
assert.strictEqual(doTheThing(), true);
247+
});
248+
```
249+
250+
`skip` and/or `todo` are mutually exclusive to `expectFailure`, and `skip` or `todo`
251+
will "win" when both are applied (`skip` wins against both, and `todo` wins
252+
against `expectFailure`).
253+
254+
These tests will be skipped (and not run):
255+
256+
```js
257+
it.expectFailure('should do the thing', { skip: true }, () => {
258+
assert.strictEqual(doTheThing(), true);
259+
});
260+
261+
it.skip('should do the thing', { expectFailure: true }, () => {
262+
assert.strictEqual(doTheThing(), true);
263+
});
264+
```
265+
266+
These tests will be marked "todo" (silencing errors):
267+
268+
```js
269+
it.expectFailure('should do the thing', { todo: true }, () => {
270+
assert.strictEqual(doTheThing(), true);
271+
});
272+
273+
it.todo('should do the thing', { expectFailure: true }, () => {
274+
assert.strictEqual(doTheThing(), true);
275+
});
276+
```
277+
227278
## `describe()` and `it()` aliases
228279

229280
Suites and tests can also be written using the `describe()` and `it()`

lib/internal/test_runner/harness.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ function runInParentContext(Factory) {
377377

378378
return run(name, options, fn, overrides);
379379
};
380-
ArrayPrototypeForEach(['skip', 'todo', 'only'], (keyword) => {
380+
ArrayPrototypeForEach(['expectFailure', 'skip', 'todo', 'only'], (keyword) => {
381381
test[keyword] = (name, options, fn) => {
382382
const overrides = {
383383
__proto__: null,

lib/internal/test_runner/reporter/tap.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ async function * tapReporter(source) {
3333
for await (const { type, data } of source) {
3434
switch (type) {
3535
case 'test:fail': {
36-
yield reportTest(data.nesting, data.testNumber, 'not ok', data.name, data.skip, data.todo);
36+
yield reportTest(data.nesting, data.testNumber, 'not ok', data.name, data.skip, data.todo, data.expectFailure);
3737
const location = data.file ? `${data.file}:${data.line}:${data.column}` : null;
3838
yield reportDetails(data.nesting, data.details, location);
3939
break;
4040
} case 'test:pass':
41-
yield reportTest(data.nesting, data.testNumber, 'ok', data.name, data.skip, data.todo);
41+
yield reportTest(data.nesting, data.testNumber, 'ok', data.name, data.skip, data.todo, data.expectFailure);
4242
yield reportDetails(data.nesting, data.details, null);
4343
break;
4444
case 'test:plan':
@@ -65,7 +65,7 @@ async function * tapReporter(source) {
6565
}
6666
}
6767

68-
function reportTest(nesting, testNumber, status, name, skip, todo) {
68+
function reportTest(nesting, testNumber, status, name, skip, todo, expectFailure) {
6969
let line = `${indent(nesting)}${status} ${testNumber}`;
7070

7171
if (name) {
@@ -76,6 +76,8 @@ function reportTest(nesting, testNumber, status, name, skip, todo) {
7676
line += ` # SKIP${typeof skip === 'string' && skip.length ? ` ${tapEscape(skip)}` : ''}`;
7777
} else if (todo !== undefined) {
7878
line += ` # TODO${typeof todo === 'string' && todo.length ? ` ${tapEscape(todo)}` : ''}`;
79+
} else if (expectFailure !== undefined) {
80+
line += ' # EXPECTED FAILURE';
7981
}
8082

8183
line += '\n';

lib/internal/test_runner/reporter/utils.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,16 @@ function formatError(error, indent) {
7070
function formatTestReport(type, data, showErrorDetails = true, prefix = '', indent = '') {
7171
let color = reporterColorMap[type] ?? colors.white;
7272
let symbol = reporterUnicodeSymbolMap[type] ?? ' ';
73-
const { skip, todo } = data;
73+
const { skip, todo, expectFailure } = data;
7474
const duration_ms = data.details?.duration_ms ? ` ${colors.gray}(${data.details.duration_ms}ms)${colors.white}` : '';
7575
let title = `${data.name}${duration_ms}`;
7676

7777
if (skip !== undefined) {
7878
title += ` # ${typeof skip === 'string' && skip.length ? skip : 'SKIP'}`;
7979
} else if (todo !== undefined) {
8080
title += ` # ${typeof todo === 'string' && todo.length ? todo : 'TODO'}`;
81+
} else if (expectFailure !== undefined) {
82+
title += ` # EXPECTED FAILURE`;
8183
}
8284

8385
const err = showErrorDetails && data.details?.error ? formatError(data.details.error, indent) : '';

lib/internal/test_runner/test.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ class Test extends AsyncResource {
496496
super('Test');
497497

498498
let { fn, name, parent } = options;
499-
const { concurrency, entryFile, loc, only, timeout, todo, skip, signal, plan } = options;
499+
const { concurrency, entryFile, expectFailure, loc, only, timeout, todo, skip, signal, plan } = options;
500500

501501
if (typeof fn !== 'function') {
502502
fn = noop;
@@ -635,6 +635,7 @@ class Test extends AsyncResource {
635635
this.plan = null;
636636
this.expectedAssertions = plan;
637637
this.cancelled = false;
638+
this.expectFailure = expectFailure !== undefined && expectFailure !== false;
638639
this.skipped = skip !== undefined && skip !== false;
639640
this.isTodo = (todo !== undefined && todo !== false) || this.parent?.isTodo;
640641
this.startTime = null;
@@ -946,7 +947,12 @@ class Test extends AsyncResource {
946947
return;
947948
}
948949

949-
this.passed = false;
950+
if (this.expectFailure === true) {
951+
this.passed = true;
952+
} else {
953+
this.passed = false;
954+
}
955+
950956
this.error = err;
951957
}
952958

@@ -1343,6 +1349,8 @@ class Test extends AsyncResource {
13431349
directive = this.reporter.getSkip(this.message);
13441350
} else if (this.isTodo) {
13451351
directive = this.reporter.getTodo(this.message);
1352+
} else if (this.expectFailure) {
1353+
directive = this.reporter.getXFail(this.expectFailure); // TODO(@JakobJingleheimer): support specifying failure
13461354
}
13471355

13481356
if (this.reportedType) {
@@ -1357,6 +1365,7 @@ class Test extends AsyncResource {
13571365
if (this.passedAttempt !== undefined) {
13581366
details.passed_on_attempt = this.passedAttempt;
13591367
}
1368+
13601369
return { __proto__: null, details, directive };
13611370
}
13621371

lib/internal/test_runner/tests_stream.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ class TestsStream extends Readable {
8787
return { __proto__: null, todo: reason ?? true };
8888
}
8989

90+
getXFail(expectation = undefined) {
91+
return { __proto__: null, expectFailure: expectation ?? true };
92+
}
93+
9094
enqueue(nesting, loc, name, type) {
9195
this[kEmitMessage]('test:enqueue', {
9296
__proto__: null,

test/fixtures/test-runner/output/describe_it.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,23 @@ const { describe, it, test } = require('node:test');
55
const util = require('util');
66

77

8-
it.todo('sync pass todo', () => {
8+
it.expectFailure('sync expect fail (method)', () => {
9+
throw new Error('should pass');
10+
});
11+
12+
it('sync expect fail (options)', { expectFailure: true }, () => {
13+
throw new Error('should pass');
14+
});
915

16+
it.expectFailure('async expect fail (method)', async () => {
17+
throw new Error('should pass');
18+
});
19+
20+
it('async expect fail (options)', { expectFailure: true }, async () => {
21+
throw new Error('should pass');
22+
});
23+
24+
it.todo('sync pass todo', () => {
1025
});
1126

1227
it('sync pass todo with message', { todo: 'this is a passing todo' }, () => {
@@ -16,13 +31,21 @@ it.todo('sync todo', () => {
1631
throw new Error('should not count as a failure');
1732
});
1833

34+
it.todo('sync todo with expect fail', { expectFailure: true }, () => {
35+
throw new Error('should not count as an expected failure');
36+
});
37+
1938
it('sync todo with message', { todo: 'this is a failing todo' }, () => {
2039
throw new Error('should not count as a failure');
2140
});
2241

2342
it.skip('sync skip pass', () => {
2443
});
2544

45+
it.skip('sync skip expect fail', { expectFailure: true }, () => {
46+
throw new Error('should not fail');
47+
});
48+
2649
it('sync skip pass with message', { skip: 'this is skipped' }, () => {
2750
});
2851

0 commit comments

Comments
 (0)