GeezerVison is showing github.com — test_runner: expose expectFailure message · nodejs/node@fce2930
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/test.md +62-4Lines changed: 62 additions & 4 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -265,11 +265,12 @@ added: | 265 | 265 | - v25.5.0 | 266 | 266 | --> | 267 | 267 |
| 268 | | -This flips the pass/fail reporting for a specific test or suite: A flagged test/test-case must throw | 269 | | -in order to "pass"; a test/test-case that does not throw, fails. | | 268 | +This flips the pass/fail reporting for a specific test or suite: a flagged test | | 269 | +case must throw in order to pass, and a flagged test case that does not throw | | 270 | +fails. | 270 | 271 |
| 271 | | -In the following, `doTheThing()` returns _currently_ `false` (`false` does not equal `true`, causing | 272 | | -`strictEqual` to throw, so the test-case passes). | | 272 | +In each of the following, `doTheThing()` fails to return `true`, but since the | | 273 | +tests are flagged `expectFailure`, they pass. | 273 | 274 |
| 274 | 275 | ```js | 275 | 276 | it.expectFailure('should do the thing', () => { | @@ -279,6 +280,50 @@ it.expectFailure('should do the thing', () => { | 279 | 280 | it('should do the thing', { expectFailure: true }, () => { | 280 | 281 | assert.strictEqual(doTheThing(), true); | 281 | 282 | }); | | 283 | + | | 284 | +it('should do the thing', { expectFailure: 'feature not implemented' }, () => { | | 285 | + assert.strictEqual(doTheThing(), true); | | 286 | +}); | | 287 | +``` | | 288 | + | | 289 | +If the value of `expectFailure` is a | | 290 | +[<RegExp>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp) | | | 291 | +[<Function>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function) | | | 292 | +[<Object>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) | | | 293 | +[<Error>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error), | | 294 | +the tests will pass only if they throw a matching value. | | 295 | +See [`assert.throws`][] for how each value type is handled. | | 296 | + | | 297 | +Each of the following tests fails _despite_ being flagged `expectFailure` | | 298 | +because the failure does not match the specific **expected** failure. | | 299 | + | | 300 | +```js | | 301 | +it('fails because regex does not match', { | | 302 | + expectFailure: /expected message/, | | 303 | +}, () => { | | 304 | + throw new Error('different message'); | | 305 | +}); | | 306 | + | | 307 | +it('fails because object matcher does not match', { | | 308 | + expectFailure: { code: 'ERR_EXPECTED' }, | | 309 | +}, () => { | | 310 | + const err = new Error('boom'); | | 311 | + err.code = 'ERR_ACTUAL'; | | 312 | + throw err; | | 313 | +}); | | 314 | +``` | | 315 | + | | 316 | +To supply both a reason and specific error for `expectFailure`, use `{ label, match }`. | | 317 | + | | 318 | +```js | | 319 | +it('should fail with specific error and reason', { | | 320 | + expectFailure: { | | 321 | + label: 'reason for failure', | | 322 | + match: /error message/, | | 323 | + }, | | 324 | +}, () => { | | 325 | + assert.strictEqual(doTheThing(), true); | | 326 | +}); | 282 | 327 | ``` | 283 | 328 |
| 284 | 329 | `skip` and/or `todo` are mutually exclusive to `expectFailure`, and `skip` or `todo` | @@ -1684,6 +1729,18 @@ changes: | 1684 | 1729 | thread. If `false`, only one test runs at a time. | 1685 | 1730 | If unspecified, subtests inherit this value from their parent. | 1686 | 1731 | **Default:** `false`. | | 1732 | + * `expectFailure` {boolean|string|RegExp|Function|Object|Error} If truthy, the | | 1733 | + test is expected to fail. If a non-empty string is provided, that string is displayed | | 1734 | + in the test results as the reason why the test is expected to fail. If a | | 1735 | + [<RegExp>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp), | | 1736 | + [<Function>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function), | | 1737 | + [<Object>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object), or | | 1738 | + [<Error>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) | | 1739 | + is provided directly (without wrapping in `{ match: … }`), the test passes | | 1740 | + only if the thrown error matches, following the behavior of | | 1741 | + [`assert.throws`][]. To provide both a reason and validation, pass an object | | 1742 | + with `label` (string) and `match` (RegExp, Function, Object, or Error). | | 1743 | + **Default:** `false`. | 1687 | 1744 | * `only` {boolean} If truthy, and the test context is configured to run | 1688 | 1745 | `only` tests, then this test will be run. Otherwise, the test is skipped. | 1689 | 1746 | **Default:** `false`. | @@ -4181,6 +4238,7 @@ Can be used to abort test subtasks when the test has been aborted. | 4181 | 4238 | [`NODE_V8_COVERAGE`]: cli.md#node_v8_coveragedir | 4182 | 4239 | [`SuiteContext`]: #class-suitecontext | 4183 | 4240 | [`TestContext`]: #class-testcontext | | 4241 | +[`assert.throws`]: assert.md#assertthrowsfn-error-message | 4184 | 4242 | [`context.diagnostic`]: #contextdiagnosticmessage | 4185 | 4243 | [`context.skip`]: #contextskipmessage | 4186 | 4244 | [`context.todo`]: #contexttodomessage | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/test_runner/reporter/tap.js +1-1Lines changed: 1 addition & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -87,7 +87,7 @@ function reportTest(nesting, testNumber, status, name, skip, todo, expectFailure | 87 | 87 | } else if (todo !== undefined) { | 88 | 88 | line += ` # TODO${typeof todo === 'string' && todo.length ? ` ${tapEscape(todo)}` : ''}`; | 89 | 89 | } else if (expectFailure !== undefined) { | 90 | | - line += ' # EXPECTED FAILURE'; | | 90 | + line += ` # EXPECTED FAILURE${typeof expectFailure === 'string' ? ` ${tapEscape(expectFailure)}` : ''}`; | 91 | 91 | } | 92 | 92 |
| 93 | 93 | line += '\n'; | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/test_runner/test.js +82-5Lines changed: 82 additions & 5 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | 'use strict'; | 2 | 2 | const { | | 3 | + ArrayPrototypeEvery, | 3 | 4 | ArrayPrototypePush, | 4 | 5 | ArrayPrototypePushApply, | 5 | 6 | ArrayPrototypeShift, | @@ -13,6 +14,7 @@ const { | 13 | 14 | MathMax, | 14 | 15 | Number, | 15 | 16 | NumberPrototypeToFixed, | | 17 | + ObjectKeys, | 16 | 18 | ObjectSeal, | 17 | 19 | Promise, | 18 | 20 | PromisePrototypeThen, | @@ -40,6 +42,7 @@ const { | 40 | 42 | AbortError, | 41 | 43 | codes: { | 42 | 44 | ERR_INVALID_ARG_TYPE, | | 45 | + ERR_INVALID_ARG_VALUE, | 43 | 46 | ERR_TEST_FAILURE, | 44 | 47 | }, | 45 | 48 | } = require('internal/errors'); | @@ -56,7 +59,8 @@ const { | 56 | 59 | once: runOnce, | 57 | 60 | setOwnProperty, | 58 | 61 | } = require('internal/util'); | 59 | | -const { isPromise } = require('internal/util/types'); | | 62 | +const assert = require('assert'); | | 63 | +const { isPromise, isRegExp } = require('internal/util/types'); | 60 | 64 | const { | 61 | 65 | validateAbortSignal, | 62 | 66 | validateFunction, | @@ -492,6 +496,39 @@ class SuiteContext { | 492 | 496 | } | 493 | 497 | } | 494 | 498 |
| | 499 | +function parseExpectFailure(expectFailure) { | | 500 | + if (expectFailure === undefined || expectFailure === false) { | | 501 | + return false; | | 502 | + } | | 503 | + | | 504 | + if (typeof expectFailure === 'string') { | | 505 | + return { __proto__: null, label: expectFailure, match: undefined }; | | 506 | + } | | 507 | + | | 508 | + if (typeof expectFailure === 'function' || isRegExp(expectFailure)) { | | 509 | + return { __proto__: null, label: undefined, match: expectFailure }; | | 510 | + } | | 511 | + | | 512 | + if (typeof expectFailure !== 'object') { | | 513 | + return { __proto__: null, label: undefined, match: undefined }; | | 514 | + } | | 515 | + | | 516 | + const keys = ObjectKeys(expectFailure); | | 517 | + if (keys.length === 0) { | | 518 | + throw new ERR_INVALID_ARG_VALUE('options.expectFailure', expectFailure, 'must not be an empty object'); | | 519 | + } | | 520 | + | | 521 | + if (ArrayPrototypeEvery(keys, (k) => k === 'match' || k === 'label')) { | | 522 | + return { | | 523 | + __proto__: null, | | 524 | + label: expectFailure.label, | | 525 | + match: expectFailure.match, | | 526 | + }; | | 527 | + } | | 528 | + | | 529 | + return { __proto__: null, label: undefined, match: expectFailure }; | | 530 | +} | | 531 | + | 495 | 532 | class Test extends AsyncResource { | 496 | 533 | reportedType = 'test'; | 497 | 534 | abortController; | @@ -641,7 +678,7 @@ class Test extends AsyncResource { | 641 | 678 | this.plan = null; | 642 | 679 | this.expectedAssertions = plan; | 643 | 680 | this.cancelled = false; | 644 | | - this.expectFailure = expectFailure !== undefined && expectFailure !== false; | | 681 | + this.expectFailure = parseExpectFailure(expectFailure) || this.parent?.expectFailure; | 645 | 682 | this.skipped = skip !== undefined && skip !== false; | 646 | 683 | this.isTodo = (todo !== undefined && todo !== false) || this.parent?.isTodo; | 647 | 684 | this.startTime = null; | @@ -955,7 +992,30 @@ class Test extends AsyncResource { | 955 | 992 | return; | 956 | 993 | } | 957 | 994 |
| 958 | | - if (this.expectFailure === true) { | | 995 | + if (this.expectFailure) { | | 996 | + if (typeof this.expectFailure === 'object' && | | 997 | + this.expectFailure.match !== undefined) { | | 998 | + const { match: validation } = this.expectFailure; | | 999 | + try { | | 1000 | + const errorToCheck = ( | | 1001 | + err?.code === 'ERR_TEST_FAILURE' && | | 1002 | + err?.failureType === kTestCodeFailure && | | 1003 | + err.cause | | 1004 | + ) ? | | 1005 | + err.cause : | | 1006 | + err; | | 1007 | + // eslint-disable-next-line no-restricted-syntax | | 1008 | + assert.throws(() => { throw errorToCheck; }, validation); | | 1009 | + } catch (e) { | | 1010 | + this.passed = false; | | 1011 | + this.error = new ERR_TEST_FAILURE( | | 1012 | + 'The test failed, but the error did not match the expected validation', | | 1013 | + kTestCodeFailure, | | 1014 | + ); | | 1015 | + this.error.cause = e; | | 1016 | + return; | | 1017 | + } | | 1018 | + } | 959 | 1019 | this.passed = true; | 960 | 1020 | } else { | 961 | 1021 | this.passed = false; | @@ -965,7 +1025,7 @@ class Test extends AsyncResource { | 965 | 1025 | } | 966 | 1026 |
| 967 | 1027 | pass() { | 968 | | - if (this.error == null && this.expectFailure === true && !this.skipped) { | | 1028 | + if (this.error == null && this.expectFailure && !this.skipped) { | 969 | 1029 | this.passed = false; | 970 | 1030 | this.error = new ERR_TEST_FAILURE( | 971 | 1031 | 'test was expected to fail but passed', | @@ -977,6 +1037,20 @@ class Test extends AsyncResource { | 977 | 1037 | return; | 978 | 1038 | } | 979 | 1039 |
| | 1040 | + if (this.skipped || this.isTodo) { | | 1041 | + this.passed = true; | | 1042 | + return; | | 1043 | + } | | 1044 | + | | 1045 | + if (this.expectFailure) { | | 1046 | + this.passed = false; | | 1047 | + this.error = new ERR_TEST_FAILURE( | | 1048 | + 'Test passed but was expected to fail', | | 1049 | + kTestCodeFailure, | | 1050 | + ); | | 1051 | + return; | | 1052 | + } | | 1053 | + | 980 | 1054 | this.passed = true; | 981 | 1055 | } | 982 | 1056 |
| @@ -1366,7 +1440,10 @@ class Test extends AsyncResource { | 1366 | 1440 | } else if (this.isTodo) { | 1367 | 1441 | directive = this.reporter.getTodo(this.message); | 1368 | 1442 | } else if (this.expectFailure) { | 1369 | | - directive = this.reporter.getXFail(this.expectFailure); // TODO(@JakobJingleheimer): support specifying failure | | 1443 | + const message = typeof this.expectFailure === 'object' ? | | 1444 | + this.expectFailure.label : | | 1445 | + this.expectFailure; | | 1446 | + directive = this.reporter.getXFail(message); | 1370 | 1447 | } | 1371 | 1448 |
| 1372 | 1449 | if (this.reportedType) { | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-runner-expect-error-but-pass.js +1-2Lines changed: 1 addition & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -8,10 +8,9 @@ if (!process.env.NODE_TEST_CONTEXT) { | 8 | 8 |
| 9 | 9 | stream.on('test:pass', common.mustNotCall()); | 10 | 10 | stream.on('test:fail', common.mustCall((event) => { | 11 | | - assert.strictEqual(event.expectFailure, true); | 12 | 11 | assert.strictEqual(event.details.error.code, 'ERR_TEST_FAILURE'); | 13 | 12 | assert.strictEqual(event.details.error.failureType, 'expectedFailure'); | 14 | | - assert.strictEqual(event.details.error.cause, 'test was expected to fail but passed'); | | 13 | + assert.strictEqual(event.details.error.message, 'test was expected to fail but passed'); | 15 | 14 | }, 1)); | 16 | 15 | } else { | 17 | 16 | test('passing test', { expectFailure: true }, () => {}); | |
|