Skip to content

Commit c753b42

Browse files
authored
Merge pull request #247 from eps1lon/feat/testcase-properties
feat: Allow adding `<properties>` to `<testcase>`
2 parents 64b7cf0 + 5775c62 commit c753b42

File tree

6 files changed

+244
-7
lines changed

6 files changed

+244
-7
lines changed

README.md

+33-4
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ Reporter options should also be strings exception for suiteNameTemplate, classNa
7575
| `JEST_JUNIT_REPORT_TEST_SUITE_ERRORS` | `reportTestSuiteErrors` | Reports test suites that failed to execute altogether as `error`. _Note:_ since the suite name cannot be determined from files that fail to load, it will default to file path.| `false` | N/A
7676
| `JEST_JUNIT_NO_STACK_TRACE` | `noStackTrace` | Omit stack traces from test failure reports, similar to `jest --noStackTrace` | `false` | N/A
7777
| `JEST_USE_PATH_FOR_SUITE_NAME` | `usePathForSuiteName` | **DEPRECATED. Use `suiteNameTemplate` instead.** Use file path as the `name` attribute of `<testsuite>` | `"false"` | N/A
78-
| `JEST_JUNIT_TEST_SUITE_PROPERTIES_JSON_FILE` | `testSuitePropertiesFile` | Name of the custom testsuite properties file | `"junitProperties.js"` | N/A
78+
| `JEST_JUNIT_TEST_CASE_PROPERTIES_JSON_FILE` | `testCasePropertiesFile` | Name of the custom testcase properties file | `"junitProperties.js"` | N/A
79+
| `JEST_JUNIT_TEST_CASE_PROPERTIES_DIR` | `testCasePropertiesDirectory` | Location of the custom testcase properties file | `process.cwd()` | N/A
80+
| `JEST_JUNIT_TEST_SUITE_PROPERTIES_JSON_FILE` | `testSuitePropertiesFile` | Name of the custom testsuite properties file | `"junitTestCaseProperties.js"` | N/A
7981
| `JEST_JUNIT_TEST_SUITE_PROPERTIES_DIR` | `testSuitePropertiesDirectory` | Location of the custom testsuite properties file | `process.cwd()` | N/A
8082

8183

@@ -236,9 +238,9 @@ Create a file in your project root directory named junitProperties.js:
236238
```js
237239
module.exports = () => {
238240
return {
239-
key: "value"
240-
}
241-
});
241+
key: "value",
242+
};
243+
};
242244
```
243245

244246
Will render
@@ -254,4 +256,31 @@ Will render
254256
</testsuites>
255257
```
256258

259+
#### Adding custom testcase properties
260+
261+
Create a file in your project root directory named junitTestCaseProperties.js:
262+
```js
263+
module.exports = (testResult) => {
264+
return {
265+
"dd_tags[test.invocations]": testResult.invocations,
266+
};
267+
};
268+
```
269+
270+
Will render
271+
```xml
272+
<testsuites name="jest tests">
273+
<testsuite name="addition" tests="1" errors="0" failures="0" skipped="0" timestamp="2017-07-13T09:42:28" time="0.161">
274+
<testcase classname="addition positive numbers should add up" name="addition positive numbers should add up" time="0.004">
275+
<properties>
276+
<property name="dd_tags[test.invocations]" value="1" />
277+
</properties>
278+
</testcase>
279+
</testsuite>
280+
</testsuites>
281+
```
282+
283+
WARNING: Properties for testcases is not following standard JUnit XML schema.
284+
However, other consumers may support properties for testcases like [DataDog metadata through `<property>` elements](https://docs.datadoghq.com/continuous_integration/tests/junit_upload/?tab=jenkins#providing-metadata-through-property-elements)
285+
257286
[test-results-processor]: https://github.com/jest-community/jest-junit/discussions/158#discussioncomment-392985

__mocks__/retried-tests.json

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
{
2+
"numFailedTestSuites": 0,
3+
"numFailedTests": 0,
4+
"numPassedTestSuites": 1,
5+
"numPassedTests": 1,
6+
"numPendingTestSuites": 0,
7+
"numPendingTests": 0,
8+
"numRuntimeErrorTestSuites": 0,
9+
"numTotalTestSuites": 1,
10+
"numTotalTests": 1,
11+
"snapshot": {
12+
"added": 0,
13+
"failure": false,
14+
"filesAdded": 0,
15+
"filesRemoved": 0,
16+
"filesUnmatched": 0,
17+
"filesUpdated": 0,
18+
"matched": 0,
19+
"total": 0,
20+
"unchecked": 0,
21+
"unmatched": 0,
22+
"updated": 0
23+
},
24+
"startTime": 1489712747092,
25+
"success": true,
26+
"testResults": [
27+
{
28+
"console": null,
29+
"failureMessage": null,
30+
"numFailingTests": 0,
31+
"numPassingTests": 1,
32+
"numPendingTests": 0,
33+
"perfStats": {
34+
"end": 1489712747644,
35+
"start": 1489712747524
36+
},
37+
"snapshot": {
38+
"added": 0,
39+
"fileDeleted": false,
40+
"matched": 0,
41+
"unchecked": 0,
42+
"unmatched": 0,
43+
"updated": 0
44+
},
45+
"testFilePath": "/path/to/test/__tests__/foo.test.js",
46+
"testResults": [
47+
{
48+
"ancestorTitles": [
49+
"foo",
50+
"baz"
51+
],
52+
"duration": 1,
53+
"failureMessages": [],
54+
"fullName": "foo baz should bar",
55+
"numPassingAsserts": 0,
56+
"status": "passed",
57+
"title": "should bar",
58+
"invocations": 2,
59+
"retryReasons": ["error"]
60+
}
61+
],
62+
"skipped": false
63+
}
64+
],
65+
"wasInterrupted": false
66+
}

__tests__/buildJsonResults.test.js

+80
Original file line numberDiff line numberDiff line change
@@ -439,4 +439,84 @@ describe('buildJsonResults', () => {
439439

440440
expect(jsonResults.testsuites[1].testsuite[2]['system-out']).not.toBeDefined();
441441
});
442+
443+
it("should add properties to testcase (non standard)", () => {
444+
const retriedTestsReport = require("../__mocks__/retried-tests.json");
445+
// <properties> in <testcase> is not compatible JUnit but can be consumed by some e.g. DataDog
446+
ignoreJunitErrors = true;
447+
// Mock Date.now() to return a fixed later value
448+
const startDate = new Date(retriedTestsReport.startTime);
449+
jest.spyOn(Date, 'now').mockImplementation(() => startDate.getTime() + 1234);
450+
451+
jsonResults = buildJsonResults(retriedTestsReport, "/", {
452+
...constants.DEFAULT_OPTIONS,
453+
testCasePropertiesFile: "junitDataDogInvocationsProperties.js",
454+
});
455+
456+
expect(jsonResults).toMatchInlineSnapshot(`
457+
Object {
458+
"testsuites": Array [
459+
Object {
460+
"_attr": Object {
461+
"errors": 0,
462+
"failures": 0,
463+
"name": "jest tests",
464+
"tests": 1,
465+
"time": 1.234,
466+
},
467+
},
468+
Object {
469+
"testsuite": Array [
470+
Object {
471+
"_attr": Object {
472+
"errors": 0,
473+
"failures": 0,
474+
"name": "foo",
475+
"skipped": 0,
476+
"tests": 1,
477+
"time": 0.12,
478+
"timestamp": "2017-03-17T01:05:47",
479+
},
480+
},
481+
Object {
482+
"properties": Array [
483+
Object {
484+
"property": Object {
485+
"_attr": Object {
486+
"name": "best-tester",
487+
"value": "Jason Palmer",
488+
},
489+
},
490+
},
491+
],
492+
},
493+
Object {
494+
"testcase": Array [
495+
Object {
496+
"_attr": Object {
497+
"classname": "foo baz should bar",
498+
"name": "foo baz should bar",
499+
"time": 0.001,
500+
},
501+
},
502+
Object {
503+
"properties": Array [
504+
Object {
505+
"property": Object {
506+
"_attr": Object {
507+
"name": "dd_tags[test.invocations]",
508+
"value": 2,
509+
},
510+
},
511+
},
512+
],
513+
},
514+
],
515+
},
516+
],
517+
},
518+
],
519+
}
520+
`)
521+
});
442522
});

constants/index.js

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ module.exports = {
1818
JEST_JUNIT_REPORT_TEST_SUITE_ERRORS: 'reportTestSuiteErrors',
1919
JEST_JUNIT_NO_STACK_TRACE: "noStackTrace",
2020
JEST_USE_PATH_FOR_SUITE_NAME: 'usePathForSuiteName',
21+
JEST_JUNIT_TEST_CASE_PROPERTIES_JSON_FILE: 'testCasePropertiesFile',
22+
JEST_JUNIT_TEST_CASE_PROPERTIES_DIR: 'testCasePropertiesDirectory',
2123
JEST_JUNIT_TEST_SUITE_PROPERTIES_JSON_FILE: 'testSuitePropertiesFile',
2224
JEST_JUNIT_TEST_SUITE_PROPERTIES_DIR: 'testSuitePropertiesDirectory',
2325
},
@@ -37,6 +39,8 @@ module.exports = {
3739
includeShortConsoleOutput: 'false',
3840
reportTestSuiteErrors: 'false',
3941
noStackTrace: 'false',
42+
testCasePropertiesFile: 'junitTestCaseProperties.js',
43+
testCasePropertiesDirectory: process.cwd(),
4044
testSuitePropertiesFile: 'junitProperties.js',
4145
testSuitePropertiesDirectory: process.cwd(),
4246
},

junitDataDogInvocationsProperties.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = (testResult) => {
2+
return {
3+
"dd_tags[test.invocations]": testResult.invocations,
4+
};
5+
};

utils/buildJsonResults.js

+56-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const constants = require('../constants/index');
55
const path = require('path');
66
const fs = require('fs');
77
const getTestSuitePropertiesPath = require('./getTestSuitePropertiesPath');
8+
const replaceRootDirInOutput = require('./getOptions').replaceRootDirInOutput;
89

910
// Wrap the varName with template tags
1011
const toTemplateTag = function (varName) {
@@ -38,7 +39,21 @@ const executionTime = function (startTime, endTime) {
3839
return (endTime - startTime) / 1000;
3940
}
4041

41-
const generateTestCase = function(junitOptions, suiteOptions, tc, filepath, filename, suiteTitle, displayName){
42+
const getTestCasePropertiesPath = (options, rootDir = null) => {
43+
const testCasePropertiesPath = replaceRootDirInOutput(
44+
rootDir,
45+
path.join(
46+
options.testCasePropertiesDirectory,
47+
options.testCasePropertiesFile,
48+
),
49+
);
50+
51+
return path.isAbsolute(testCasePropertiesPath)
52+
? testCasePropertiesPath
53+
: path.resolve(testCasePropertiesPath);
54+
};
55+
56+
const generateTestCase = function(junitOptions, suiteOptions, tc, filepath, filename, suiteTitle, displayName, getGetCaseProperties){
4257
const classname = tc.ancestorTitles.join(suiteOptions.ancestorSeparator);
4358
const testTitle = tc.title;
4459

@@ -87,6 +102,30 @@ const generateTestCase = function(junitOptions, suiteOptions, tc, filepath, file
87102
});
88103
}
89104

105+
if (getGetCaseProperties !== null) {
106+
let junitCaseProperties = getGetCaseProperties(tc);
107+
108+
// Add any test suite properties
109+
let testCasePropertyMain = {
110+
'properties': []
111+
};
112+
113+
Object.keys(junitCaseProperties).forEach((p) => {
114+
let testSuiteProperty = {
115+
'property': {
116+
_attr: {
117+
name: p,
118+
value: junitCaseProperties[p]
119+
}
120+
}
121+
};
122+
123+
testCasePropertyMain.properties.push(testSuiteProperty);
124+
});
125+
126+
testCase.testcase.push(testCasePropertyMain);
127+
}
128+
90129
return testCase;
91130
}
92131

@@ -115,6 +154,9 @@ module.exports = function (report, appDirectory, options, rootDir = null) {
115154
);
116155
let ignoreSuitePropertiesCheck = !fs.existsSync(junitSuitePropertiesFilePath);
117156

157+
const testCasePropertiesPath = getTestCasePropertiesPath(options, rootDir)
158+
const getTestCaseProperties = fs.existsSync(testCasePropertiesPath) ? require(testCasePropertiesPath) : null
159+
118160
// If the usePathForSuiteName option is true and the
119161
// suiteNameTemplate value is set to the default, overrides
120162
// the suiteNameTemplate.
@@ -223,7 +265,16 @@ module.exports = function (report, appDirectory, options, rootDir = null) {
223265

224266
// Iterate through test cases
225267
suite.testResults.forEach((tc) => {
226-
const testCase = generateTestCase(options, suiteOptions, tc, filepath, filename, suiteTitle, displayName)
268+
const testCase = generateTestCase(
269+
options,
270+
suiteOptions,
271+
tc,
272+
filepath,
273+
filename,
274+
suiteTitle,
275+
displayName,
276+
getTestCaseProperties
277+
);
227278
testSuite.testsuite.push(testCase);
228279
});
229280

@@ -237,6 +288,7 @@ module.exports = function (report, appDirectory, options, rootDir = null) {
237288
title: "Test execution failure: could be caused by test hooks like 'afterAll'.",
238289
ancestorTitles: [""],
239290
duration: 0,
291+
invocations: 1,
240292
};
241293
const testCase = generateTestCase(
242294
options,
@@ -245,7 +297,8 @@ module.exports = function (report, appDirectory, options, rootDir = null) {
245297
filepath,
246298
filename,
247299
suiteTitle,
248-
displayName
300+
displayName,
301+
getTestCaseProperties
249302
);
250303
testSuite.testsuite.push(testCase);
251304
}

0 commit comments

Comments
 (0)