Repository: gotestyourself/gotestsum
Pull request #478 (2025-03-13 21:41:26)

feat: support for hidding skipped tests in the junit report
### What

Add a feature flag when using the JUnit reporter to hide the skipped tests.

### Why

Utilizing a centralized Test Engine Reporter proves to be very beneficial. Some of those engine reporters take into account the duration of skipped tests, which is important for maintaining accurate data. In scenarios where tests are periodically enabled or disabled and may experience some flakiness, the average duration can be skewed if the same tests are skipped during specific timeframes. By having the capability to exclude these skipped tests, we can obtain a clearer and more accurate reflection of test durations over time, ultimately improving our reporting and analysis.


Patch:
diff --git a/cmd/handler.go b/cmd/handler.go
index b70cd27..5ec1cf3 100644
--- a/cmd/handler.go
+++ b/cmd/handler.go
@@ -153,6 +153,7 @@ func writeJUnitFile(opts *options, execution *testjson.Execution) error {
 		FormatTestSuiteName:     opts.junitTestSuiteNameFormat.Value(),
 		FormatTestCaseClassname: opts.junitTestCaseClassnameFormat.Value(),
 		HideEmptyPackages:       opts.junitHideEmptyPackages,
+		HideSkippedTests:        opts.junitHideSkippedTests,
 	})
 }
 
diff --git a/cmd/main.go b/cmd/main.go
index 965b7f8..71695e0 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -108,6 +108,9 @@ func setupFlags(name string) (*pflag.FlagSet, *options) {
 	flags.BoolVar(&opts.junitHideEmptyPackages, "junitfile-hide-empty-pkg",
 		truthyFlag(lookEnvWithDefault("GOTESTSUM_JUNIT_HIDE_EMPTY_PKG", "")),
 		"omit packages with no tests from the junit.xml file")
+	flags.BoolVar(&opts.junitHideSkippedTests, "junitfile-hide-skipped-tests",
+		truthyFlag(lookEnvWithDefault("GOTESTSUM_JUNIT_HIDE_SKIPPED_TESTS", "")),
+		"omit skipped tests from the junit.xml file")
 
 	flags.IntVar(&opts.rerunFailsMaxAttempts, "rerun-fails", 0,
 		"rerun failed tests until they all pass, or attempts exceeds maximum. Defaults to max 2 reruns when enabled")
@@ -187,6 +190,7 @@ type options struct {
 	junitTestCaseClassnameFormat *junitFieldFormatValue
 	junitProjectName             string
 	junitHideEmptyPackages       bool
+	junitHideSkippedTests        bool
 	rerunFailsMaxAttempts        int
 	rerunFailsMaxInitialFailures int
 	rerunFailsReportFile         string
diff --git a/internal/junitxml/report.go b/internal/junitxml/report.go
index c5db2cc..db46604 100644
--- a/internal/junitxml/report.go
+++ b/internal/junitxml/report.go
@@ -75,6 +75,7 @@ type Config struct {
 	FormatTestSuiteName     FormatFunc
 	FormatTestCaseClassname FormatFunc
 	HideEmptyPackages       bool
+	HideSkippedTests        bool
 	// This is used for tests to have a consistent timestamp
 	customTimestamp string
 	customElapsed   string
@@ -94,9 +95,15 @@ func Write(out io.Writer, exec *testjson.Execution, cfg Config) error {
 func generate(exec *testjson.Execution, cfg Config) JUnitTestSuites {
 	cfg = configWithDefaults(cfg)
 	version := goVersion()
+
+	total := exec.Total()
+	if cfg.HideSkippedTests && len(exec.Skipped()) > 0 {
+		total -= len(exec.Skipped())
+	}
+
 	suites := JUnitTestSuites{
 		Name:     cfg.ProjectName,
-		Tests:    exec.Total(),
+		Tests:    total,
 		Failures: len(exec.Failed()),
 		Errors:   len(exec.Errors()),
 		Time:     formatDurationAsSeconds(exec.Elapsed()),
@@ -112,6 +119,11 @@ func generate(exec *testjson.Execution, cfg Config) JUnitTestSuites {
 			continue
 		}
 
+		if cfg.HideSkippedTests && len(pkg.Skipped) > 0 {
+			pkg.Total -= len(pkg.Skipped)
+			pkg.Skipped = nil
+		}
+
 		junitpkg := JUnitTestSuite{
 			Name:       cfg.FormatTestSuiteName(pkgname),
 			Tests:      pkg.Total,


Test patch:
diff --git a/cmd/testdata/gotestsum-help-text b/cmd/testdata/gotestsum-help-text
index 3a1fde6..35d4482 100644
--- a/cmd/testdata/gotestsum-help-text
+++ b/cmd/testdata/gotestsum-help-text
@@ -14,6 +14,7 @@ Flags:
       --jsonfile-timing-events string               write only the pass, skip, and fail TestEvents to the file
       --junitfile string                            write a JUnit XML file
       --junitfile-hide-empty-pkg                    omit packages with no tests from the junit.xml file
+      --junitfile-hide-skipped-tests                omit skipped tests from the junit.xml file
       --junitfile-project-name string               name of the project used in the junit.xml file
       --junitfile-testcase-classname field-format   format the testcase classname field as: full, relative, short (default full)
       --junitfile-testsuite-name field-format       format the testsuite name field as: full, relative, short (default full)
diff --git a/internal/junitxml/report_test.go b/internal/junitxml/report_test.go
index 2d3c683..01539c3 100644
--- a/internal/junitxml/report_test.go
+++ b/internal/junitxml/report_test.go
@@ -43,6 +43,21 @@ func TestWrite_HideEmptyPackages(t *testing.T) {
 	golden.Assert(t, out.String(), "junitxml-report-skip-empty.golden")
 }
 
+func TestWrite_HideSkippedTests(t *testing.T) {
+	out := new(bytes.Buffer)
+	exec := createExecution(t)
+
+	t.Setenv("GOVERSION", "go7.7.7")
+	err := Write(out, exec, Config{
+		ProjectName:      "test",
+		HideSkippedTests: true,
+		customTimestamp:  new(time.Time).Format(time.RFC3339),
+		customElapsed:    "2.1",
+	})
+	assert.NilError(t, err)
+	golden.Assert(t, out.String(), "junitxml-report-hide-skipped-tests.golden")
+}
+
 func createExecution(t *testing.T) *testjson.Execution {
 	exec, err := testjson.ScanTestOutput(testjson.ScanConfig{
 		Stdout: readTestData(t, "out"),
diff --git a/internal/junitxml/testdata/junitxml-report-hide-skipped-tests.golden b/internal/junitxml/testdata/junitxml-report-hide-skipped-tests.golden
new file mode 100644
index 0000000..6d686db
--- /dev/null
+++ b/internal/junitxml/testdata/junitxml-report-hide-skipped-tests.golden
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<testsuites name="test" tests="54" failures="13" errors="1" time="2.1">
+	<testsuite tests="0" failures="0" time="0.001000" name="gotest.tools/gotestsum/testjson/internal/badmain" timestamp="0001-01-01T00:00:00Z">
+		<properties>
+			<property name="go.version" value="go7.7.7"></property>
+		</properties>
+		<testcase classname="" name="TestMain" time="0.000000">
+			<failure message="Failed" type="">sometimes main can exit 2&#xA;FAIL&#x9;gotest.tools/gotestsum/testjson/internal/badmain&#x9;0.001s&#xA;</failure>
+		</testcase>
+	</testsuite>
+	<testsuite tests="0" failures="0" time="0.000000" name="gotest.tools/gotestsum/testjson/internal/empty" timestamp="0001-01-01T00:00:00Z">
+		<properties>
+			<property name="go.version" value="go7.7.7"></property>
+		</properties>
+	</testsuite>
+	<testsuite tests="16" failures="0" time="0.000000" name="gotest.tools/gotestsum/testjson/internal/good" timestamp="0001-01-01T00:00:00Z">
+		<properties>
+			<property name="go.version" value="go7.7.7"></property>
+		</properties>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestPassed" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestPassedWithLog" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestPassedWithStdout" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestWithStderr" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/a/sub" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/a" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/b/sub" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/b" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/c/sub" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/c" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/d/sub" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/d" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestParallelTheFirst" time="0.010000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestParallelTheThird" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestParallelTheSecond" time="0.010000"></testcase>
+	</testsuite>
+	<testsuite tests="12" failures="8" time="0.020000" name="gotest.tools/gotestsum/testjson/internal/parallelfails" timestamp="0001-01-01T00:00:00Z">
+		<properties>
+			<property name="go.version" value="go7.7.7"></property>
+		</properties>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestNestedParallelFailures/a" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestNestedParallelFailures/a&#xA;=== PAUSE TestNestedParallelFailures/a&#xA;=== CONT  TestNestedParallelFailures/a&#xA;    fails_test.go:50: failed sub a&#xA;    --- FAIL: TestNestedParallelFailures/a (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestNestedParallelFailures/d" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestNestedParallelFailures/d&#xA;=== PAUSE TestNestedParallelFailures/d&#xA;=== CONT  TestNestedParallelFailures/d&#xA;    fails_test.go:50: failed sub d&#xA;    --- FAIL: TestNestedParallelFailures/d (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestNestedParallelFailures/c" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestNestedParallelFailures/c&#xA;=== PAUSE TestNestedParallelFailures/c&#xA;=== CONT  TestNestedParallelFailures/c&#xA;    fails_test.go:50: failed sub c&#xA;    --- FAIL: TestNestedParallelFailures/c (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestNestedParallelFailures/b" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestNestedParallelFailures/b&#xA;=== PAUSE TestNestedParallelFailures/b&#xA;=== CONT  TestNestedParallelFailures/b&#xA;    fails_test.go:50: failed sub b&#xA;    --- FAIL: TestNestedParallelFailures/b (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestNestedParallelFailures" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestNestedParallelFailures&#xA;--- FAIL: TestNestedParallelFailures (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestParallelTheFirst" time="0.010000">
+			<failure message="Failed" type="">=== RUN   TestParallelTheFirst&#xA;=== PAUSE TestParallelTheFirst&#xA;=== CONT  TestParallelTheFirst&#xA;    fails_test.go:29: failed the first&#xA;--- FAIL: TestParallelTheFirst (0.01s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestParallelTheThird" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestParallelTheThird&#xA;=== PAUSE TestParallelTheThird&#xA;=== CONT  TestParallelTheThird&#xA;    fails_test.go:41: failed the third&#xA;--- FAIL: TestParallelTheThird (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestParallelTheSecond" time="0.010000">
+			<failure message="Failed" type="">=== RUN   TestParallelTheSecond&#xA;=== PAUSE TestParallelTheSecond&#xA;=== CONT  TestParallelTheSecond&#xA;    fails_test.go:35: failed the second&#xA;--- FAIL: TestParallelTheSecond (0.01s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestPassed" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestPassedWithLog" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestPassedWithStdout" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestWithStderr" time="0.000000"></testcase>
+	</testsuite>
+	<testsuite tests="26" failures="4" time="0.020000" name="gotest.tools/gotestsum/testjson/internal/withfails" timestamp="0001-01-01T00:00:00Z">
+		<properties>
+			<property name="go.version" value="go7.7.7"></property>
+		</properties>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestFailed" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestFailed&#xA;    fails_test.go:34: this failed&#xA;--- FAIL: TestFailed (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestFailedWithStderr" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestFailedWithStderr&#xA;this is stderr&#xA;    fails_test.go:43: also failed&#xA;--- FAIL: TestFailedWithStderr (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure/c" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestNestedWithFailure/c&#xA;    fails_test.go:65: failed&#xA;    --- FAIL: TestNestedWithFailure/c (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestNestedWithFailure&#xA;--- FAIL: TestNestedWithFailure (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestPassed" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestPassedWithLog" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestPassedWithStdout" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestWithStderr" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure/a/sub" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure/a" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure/b/sub" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure/b" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure/d/sub" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure/d" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/a/sub" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/a" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/b/sub" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/b" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/c/sub" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/c" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/d/sub" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/d" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestParallelTheFirst" time="0.010000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestParallelTheThird" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestParallelTheSecond" time="0.010000"></testcase>
+	</testsuite>
+</testsuites>
\ No newline at end of file