From fa3594e1b0f10ba96a553755d31f3a5632fa4968 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Wed, 22 Mar 2023 16:00:23 +0100 Subject: [PATCH 1/2] feat: Allow adding `` to `` --- README.md | 38 +++++++++++-- __mocks__/retried-tests.json | 66 +++++++++++++++++++++++ __tests__/buildJsonResults.test.js | 80 ++++++++++++++++++++++++++++ constants/index.js | 4 ++ junitDataDogInvocationsProperties.js | 5 ++ utils/buildJsonResults.js | 59 ++++++++++++++++++-- 6 files changed, 245 insertions(+), 7 deletions(-) create mode 100644 __mocks__/retried-tests.json create mode 100644 junitDataDogInvocationsProperties.js diff --git a/README.md b/README.md index 738c7e4..90aa90e 100644 --- a/README.md +++ b/README.md @@ -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 `` | `"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 @@ -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 @@ -254,4 +256,32 @@ Will render ``` +#### Adding custom testcase properties +New feature as of jest-junit 11.0.0! + +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 + + + + + + + + + +``` + +WARNING: Properties for testcases is not following standard JUnit XML schema. +However, other consumers may support properties for testcases like [DataDog metadata through `` 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 diff --git a/__mocks__/retried-tests.json b/__mocks__/retried-tests.json new file mode 100644 index 0000000..f7c044d --- /dev/null +++ b/__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 +} diff --git a/__tests__/buildJsonResults.test.js b/__tests__/buildJsonResults.test.js index ed0de19..f7a91c3 100644 --- a/__tests__/buildJsonResults.test.js +++ b/__tests__/buildJsonResults.test.js @@ -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"); + // in 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, + }, + }, + }, + ], + }, + ], + }, + ], + }, + ], + } + `) + }); }); diff --git a/constants/index.js b/constants/index.js index 1c7fe5d..fbe178e 100644 --- a/constants/index.js +++ b/constants/index.js @@ -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', }, @@ -37,6 +39,8 @@ module.exports = { includeShortConsoleOutput: 'false', reportTestSuiteErrors: 'false', noStackTrace: 'false', + testCasePropertiesFile: 'junitTestCaseProperties.js', + testCasePropertiesDirectory: process.cwd(), testSuitePropertiesFile: 'junitProperties.js', testSuitePropertiesDirectory: process.cwd(), }, diff --git a/junitDataDogInvocationsProperties.js b/junitDataDogInvocationsProperties.js new file mode 100644 index 0000000..d2e4c67 --- /dev/null +++ b/junitDataDogInvocationsProperties.js @@ -0,0 +1,5 @@ +module.exports = (testResult) => { + return { + "dd_tags[test.invocations]": testResult.invocations, + }; +}; diff --git a/utils/buildJsonResults.js b/utils/buildJsonResults.js index 6d1e59e..46d2378 100644 --- a/utils/buildJsonResults.js +++ b/utils/buildJsonResults.js @@ -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) { @@ -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; @@ -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; } @@ -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. @@ -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); }); @@ -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, @@ -245,7 +297,8 @@ module.exports = function (report, appDirectory, options, rootDir = null) { filepath, filename, suiteTitle, - displayName + displayName, + getTestCaseProperties ); testSuite.testsuite.push(testCase); } From 5775c620def7a0562c1d6394aa91a286a521d07f Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Wed, 22 Mar 2023 17:22:24 +0100 Subject: [PATCH 2/2] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 90aa90e..a20f5ee 100644 --- a/README.md +++ b/README.md @@ -257,7 +257,6 @@ Will render ``` #### Adding custom testcase properties -New feature as of jest-junit 11.0.0! Create a file in your project root directory named junitTestCaseProperties.js: ```js