Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow adding <properties> to <testcase> #247

Merged
merged 2 commits into from Apr 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 33 additions & 4 deletions README.md
Expand Up @@ -75,7 +75,9 @@ Reporter options should also be strings exception for suiteNameTemplate, classNa
| `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
| `JEST_JUNIT_NO_STACK_TRACE` | `noStackTrace` | Omit stack traces from test failure reports, similar to `jest --noStackTrace` | `false` | N/A
| `JEST_USE_PATH_FOR_SUITE_NAME` | `usePathForSuiteName` | **DEPRECATED. Use `suiteNameTemplate` instead.** Use file path as the `name` attribute of `<testsuite>` | `"false"` | N/A
| `JEST_JUNIT_TEST_SUITE_PROPERTIES_JSON_FILE` | `testSuitePropertiesFile` | Name of the custom testsuite properties file | `"junitProperties.js"` | N/A
| `JEST_JUNIT_TEST_CASE_PROPERTIES_JSON_FILE` | `testCasePropertiesFile` | Name of the custom testcase properties file | `"junitProperties.js"` | N/A
| `JEST_JUNIT_TEST_CASE_PROPERTIES_DIR` | `testCasePropertiesDirectory` | Location of the custom testcase properties file | `process.cwd()` | N/A
| `JEST_JUNIT_TEST_SUITE_PROPERTIES_JSON_FILE` | `testSuitePropertiesFile` | Name of the custom testsuite properties file | `"junitTestCaseProperties.js"` | N/A
| `JEST_JUNIT_TEST_SUITE_PROPERTIES_DIR` | `testSuitePropertiesDirectory` | Location of the custom testsuite properties file | `process.cwd()` | N/A


Expand Down Expand Up @@ -236,9 +238,9 @@ Create a file in your project root directory named junitProperties.js:
```js
module.exports = () => {
return {
key: "value"
}
});
key: "value",
};
};
```

Will render
Expand All @@ -254,4 +256,31 @@ Will render
</testsuites>
```

#### Adding custom testcase properties

Create a file in your project root directory named junitTestCaseProperties.js:
```js
module.exports = (testResult) => {
return {
"dd_tags[test.invocations]": testResult.invocations,
};
};
```

Will render
```xml
<testsuites name="jest tests">
<testsuite name="addition" tests="1" errors="0" failures="0" skipped="0" timestamp="2017-07-13T09:42:28" time="0.161">
<testcase classname="addition positive numbers should add up" name="addition positive numbers should add up" time="0.004">
<properties>
<property name="dd_tags[test.invocations]" value="1" />
</properties>
</testcase>
</testsuite>
</testsuites>
```

WARNING: Properties for testcases is not following standard JUnit XML schema.
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)

[test-results-processor]: https://github.com/jest-community/jest-junit/discussions/158#discussioncomment-392985
66 changes: 66 additions & 0 deletions __mocks__/retried-tests.json
@@ -0,0 +1,66 @@
{
"numFailedTestSuites": 0,
"numFailedTests": 0,
"numPassedTestSuites": 1,
"numPassedTests": 1,
"numPendingTestSuites": 0,
"numPendingTests": 0,
"numRuntimeErrorTestSuites": 0,
"numTotalTestSuites": 1,
"numTotalTests": 1,
"snapshot": {
"added": 0,
"failure": false,
"filesAdded": 0,
"filesRemoved": 0,
"filesUnmatched": 0,
"filesUpdated": 0,
"matched": 0,
"total": 0,
"unchecked": 0,
"unmatched": 0,
"updated": 0
},
"startTime": 1489712747092,
"success": true,
"testResults": [
{
"console": null,
"failureMessage": null,
"numFailingTests": 0,
"numPassingTests": 1,
"numPendingTests": 0,
"perfStats": {
"end": 1489712747644,
"start": 1489712747524
},
"snapshot": {
"added": 0,
"fileDeleted": false,
"matched": 0,
"unchecked": 0,
"unmatched": 0,
"updated": 0
},
"testFilePath": "/path/to/test/__tests__/foo.test.js",
"testResults": [
{
"ancestorTitles": [
"foo",
"baz"
],
"duration": 1,
"failureMessages": [],
"fullName": "foo baz should bar",
"numPassingAsserts": 0,
"status": "passed",
"title": "should bar",
"invocations": 2,
"retryReasons": ["error"]
}
],
"skipped": false
}
],
"wasInterrupted": false
}
80 changes: 80 additions & 0 deletions __tests__/buildJsonResults.test.js
Expand Up @@ -439,4 +439,84 @@ describe('buildJsonResults', () => {

expect(jsonResults.testsuites[1].testsuite[2]['system-out']).not.toBeDefined();
});

it("should add properties to testcase (non standard)", () => {
const retriedTestsReport = require("../__mocks__/retried-tests.json");
// <properties> in <testcase> is not compatible JUnit but can be consumed by some e.g. DataDog
ignoreJunitErrors = true;
// Mock Date.now() to return a fixed later value
const startDate = new Date(retriedTestsReport.startTime);
jest.spyOn(Date, 'now').mockImplementation(() => startDate.getTime() + 1234);

jsonResults = buildJsonResults(retriedTestsReport, "/", {
...constants.DEFAULT_OPTIONS,
testCasePropertiesFile: "junitDataDogInvocationsProperties.js",
});

expect(jsonResults).toMatchInlineSnapshot(`
Object {
"testsuites": Array [
Object {
"_attr": Object {
"errors": 0,
"failures": 0,
"name": "jest tests",
"tests": 1,
"time": 1.234,
},
},
Object {
"testsuite": Array [
Object {
"_attr": Object {
"errors": 0,
"failures": 0,
"name": "foo",
"skipped": 0,
"tests": 1,
"time": 0.12,
"timestamp": "2017-03-17T01:05:47",
},
},
Object {
"properties": Array [
Object {
"property": Object {
"_attr": Object {
"name": "best-tester",
"value": "Jason Palmer",
},
},
},
],
},
Object {
"testcase": Array [
Object {
"_attr": Object {
"classname": "foo baz should bar",
"name": "foo baz should bar",
"time": 0.001,
},
},
Object {
"properties": Array [
Object {
"property": Object {
"_attr": Object {
"name": "dd_tags[test.invocations]",
"value": 2,
},
},
},
],
},
],
},
],
},
],
}
`)
});
});
4 changes: 4 additions & 0 deletions constants/index.js
Expand Up @@ -18,6 +18,8 @@ module.exports = {
JEST_JUNIT_REPORT_TEST_SUITE_ERRORS: 'reportTestSuiteErrors',
JEST_JUNIT_NO_STACK_TRACE: "noStackTrace",
JEST_USE_PATH_FOR_SUITE_NAME: 'usePathForSuiteName',
JEST_JUNIT_TEST_CASE_PROPERTIES_JSON_FILE: 'testCasePropertiesFile',
JEST_JUNIT_TEST_CASE_PROPERTIES_DIR: 'testCasePropertiesDirectory',
JEST_JUNIT_TEST_SUITE_PROPERTIES_JSON_FILE: 'testSuitePropertiesFile',
JEST_JUNIT_TEST_SUITE_PROPERTIES_DIR: 'testSuitePropertiesDirectory',
},
Expand All @@ -37,6 +39,8 @@ module.exports = {
includeShortConsoleOutput: 'false',
reportTestSuiteErrors: 'false',
noStackTrace: 'false',
testCasePropertiesFile: 'junitTestCaseProperties.js',
testCasePropertiesDirectory: process.cwd(),
testSuitePropertiesFile: 'junitProperties.js',
testSuitePropertiesDirectory: process.cwd(),
},
Expand Down
5 changes: 5 additions & 0 deletions junitDataDogInvocationsProperties.js
@@ -0,0 +1,5 @@
module.exports = (testResult) => {
return {
"dd_tags[test.invocations]": testResult.invocations,
};
};
59 changes: 56 additions & 3 deletions utils/buildJsonResults.js
Expand Up @@ -5,6 +5,7 @@ const constants = require('../constants/index');
const path = require('path');
const fs = require('fs');
const getTestSuitePropertiesPath = require('./getTestSuitePropertiesPath');
const replaceRootDirInOutput = require('./getOptions').replaceRootDirInOutput;

// Wrap the varName with template tags
const toTemplateTag = function (varName) {
Expand Down Expand Up @@ -38,7 +39,21 @@ const executionTime = function (startTime, endTime) {
return (endTime - startTime) / 1000;
}

const generateTestCase = function(junitOptions, suiteOptions, tc, filepath, filename, suiteTitle, displayName){
const getTestCasePropertiesPath = (options, rootDir = null) => {
const testCasePropertiesPath = replaceRootDirInOutput(
rootDir,
path.join(
options.testCasePropertiesDirectory,
options.testCasePropertiesFile,
),
);

return path.isAbsolute(testCasePropertiesPath)
? testCasePropertiesPath
: path.resolve(testCasePropertiesPath);
};

const generateTestCase = function(junitOptions, suiteOptions, tc, filepath, filename, suiteTitle, displayName, getGetCaseProperties){
const classname = tc.ancestorTitles.join(suiteOptions.ancestorSeparator);
const testTitle = tc.title;

Expand Down Expand Up @@ -87,6 +102,30 @@ const generateTestCase = function(junitOptions, suiteOptions, tc, filepath, file
});
}

if (getGetCaseProperties !== null) {
let junitCaseProperties = getGetCaseProperties(tc);

// Add any test suite properties
let testCasePropertyMain = {
'properties': []
};

Object.keys(junitCaseProperties).forEach((p) => {
let testSuiteProperty = {
'property': {
_attr: {
name: p,
value: junitCaseProperties[p]
}
}
};

testCasePropertyMain.properties.push(testSuiteProperty);
});

testCase.testcase.push(testCasePropertyMain);
}

return testCase;
}

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

const testCasePropertiesPath = getTestCasePropertiesPath(options, rootDir)
const getTestCaseProperties = fs.existsSync(testCasePropertiesPath) ? require(testCasePropertiesPath) : null

// If the usePathForSuiteName option is true and the
// suiteNameTemplate value is set to the default, overrides
// the suiteNameTemplate.
Expand Down Expand Up @@ -223,7 +265,16 @@ module.exports = function (report, appDirectory, options, rootDir = null) {

// Iterate through test cases
suite.testResults.forEach((tc) => {
const testCase = generateTestCase(options, suiteOptions, tc, filepath, filename, suiteTitle, displayName)
const testCase = generateTestCase(
options,
suiteOptions,
tc,
filepath,
filename,
suiteTitle,
displayName,
getTestCaseProperties
);
testSuite.testsuite.push(testCase);
});

Expand All @@ -237,6 +288,7 @@ module.exports = function (report, appDirectory, options, rootDir = null) {
title: "Test execution failure: could be caused by test hooks like 'afterAll'.",
ancestorTitles: [""],
duration: 0,
invocations: 1,
};
const testCase = generateTestCase(
options,
Expand All @@ -245,7 +297,8 @@ module.exports = function (report, appDirectory, options, rootDir = null) {
filepath,
filename,
suiteTitle,
displayName
displayName,
getTestCaseProperties
);
testSuite.testsuite.push(testCase);
}
Expand Down