Skip to content

Commit c6624cc

Browse files
committed
fix: Use custom reporter to flip expected failures
To verify that the new `fail` matcher works correctly, we need a way to check if the tests failed as expected or not. Typically one would use `test.failing` for this purpose, but that only works if the test threw an exception (e.g. JestException). The `fail` matcher does not do this so that it can still work inside `catch` blocks. Instead, we have to hook into the test reporting and mutate the test results for our expected test failures prior to usage by other test reporters. This commit creates a custom test reporter which detects tests run in the file `fail.test.js` (yes the name is hard-coded) and flips the results of tests inside a `.fail` describe block (i.e. our expected failures). - If the tests failed (expected) we flip the result to a pass. - If the tests passed (unexpected) we flip the result to a fail. The custom reporter also handles the logic for updating the counts for failing test suites so that later reporters reflect the actual test results correctly.
1 parent d0474ee commit c6624cc

File tree

3 files changed

+93
-4
lines changed

3 files changed

+93
-4
lines changed

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@
103103
"watchPlugins": [
104104
"jest-watch-typeahead/filename",
105105
"jest-watch-typeahead/testname"
106+
],
107+
"reporters": [
108+
"<rootDir>/test/reporters/ExceptionlessExpectedFailureReporter.js",
109+
"default"
106110
]
107111
},
108112
"babel": {

test/matchers/fail.test.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import * as matcher from 'src/matchers/fail';
22

33
expect.extend(matcher);
4-
54
describe('.fail', () => {
6-
xtest('fails without message', () => {
5+
test('fails without message', () => {
76
expect().fail(); // This should fail!
87
});
9-
xtest('fails with message', () => {
8+
test('fails with message', () => {
109
expect().fail('This should fail!');
1110
});
12-
xtest('fails when invoked in a try/catch', () => {
11+
test('fails when invoked in a try/catch', () => {
1312
try {
1413
expect().fail();
1514
} catch (error) {
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* Flips the test results for fail.test.js > .fail > <test cases>
3+
*/
4+
class ExceptionlessExpectedFailureReporter {
5+
constructor(globalConfig, reporterOptions, reporterContext) {
6+
this._globalConfig = globalConfig;
7+
this._options = reporterOptions;
8+
this._context = reporterContext;
9+
}
10+
onTestCaseResult(test, testCaseResult) {
11+
this._processTestCaseResult(testCaseResult);
12+
}
13+
onTestFileResult(test, testResult, results) {
14+
if (this._suitePathEndsWith(testResult, 'fail.test.js')) {
15+
this._processTestResults(results);
16+
}
17+
}
18+
_suitePathEndsWith(testSuiteResult, value) {
19+
return testSuiteResult.testFilePath.endsWith('fail.test.js');
20+
}
21+
_processTestResults(results) {
22+
for (let testSuiteResult of results.testResults) {
23+
if (this._suitePathEndsWith(testSuiteResult, 'fail.test.js')) {
24+
let switchedToFailing = 0;
25+
let switchedToPassing = 0;
26+
for (let testCaseResult of testSuiteResult.testResults) {
27+
const processResult = this._processTestCaseResult(testCaseResult);
28+
if (processResult === 'switch-to-failing') switchedToFailing++;
29+
if (processResult === 'switch-to-passing') switchedToPassing++;
30+
}
31+
const originalFailureCount = testSuiteResult.numFailingTests;
32+
testSuiteResult.numFailingTests += switchedToFailing - switchedToPassing;
33+
results.numFailedTests += switchedToFailing - switchedToPassing;
34+
testSuiteResult.numPassingTests += switchedToPassing - switchedToFailing;
35+
results.numPassedTests += switchedToPassing - switchedToFailing;
36+
if (originalFailureCount === switchedToPassing) {
37+
testSuiteResult.failureMessage = '';
38+
results.numFailedTestSuites -= 1;
39+
results.numPassedTestSuites += 1;
40+
if (results.numFailedTestSuites === 0) results.success = true;
41+
console.log('marking failing test suite as passing', testSuiteResult.testFilePath);
42+
}
43+
}
44+
}
45+
}
46+
47+
_processTestCaseResult(testCaseResult) {
48+
if (this._hasDotFailAncestor(testCaseResult)) {
49+
if (testCaseResult.status === 'failed') {
50+
this._markPassing(testCaseResult);
51+
return 'switch-to-passing';
52+
} else if (testCaseResult.status === 'passed') {
53+
this._markFailing(testCaseResult);
54+
return 'switch-to-failing';
55+
}
56+
}
57+
return 'unchanged';
58+
}
59+
_hasDotFailAncestor(result) {
60+
return result.ancestorTitles.length > 0 && result.ancestorTitles[0] === '.fail';
61+
}
62+
_markPassing(result) {
63+
result.status = 'passed';
64+
result.failureDetails = [];
65+
result.failureMessages = [];
66+
result.numPassingAsserts = 1;
67+
}
68+
_markFailing(result) {
69+
const message = `${result.fullName} was expected to fail, but did not.`;
70+
result.status = 'failed';
71+
result.failureDetails = [
72+
{
73+
matcherResult: {
74+
pass: false,
75+
message: message,
76+
},
77+
message: message,
78+
stack: `${message}\n\tNo stack trace.\n\tThis is a placeholder message generated inside ExceptionlessExpectedFailureReporter`,
79+
},
80+
];
81+
result.failureMessages = [message];
82+
result.numPassingAsserts = 0;
83+
}
84+
}
85+
86+
module.exports = ExceptionlessExpectedFailureReporter;

0 commit comments

Comments
 (0)