diff --git a/changelog/3850.bugfix.rst b/changelog/3850.bugfix.rst new file mode 100644 index 00000000000..0b4fb32b095 --- /dev/null +++ b/changelog/3850.bugfix.rst @@ -0,0 +1 @@ +Fixed the JUnit XML ``tests`` count for tests that pass during call but fail during teardown. diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index ae8d2b94d36..aad3ed211ee 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -578,7 +578,17 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: # call and error in teardown in order to follow junit # schema. self.finalize(close_report) - self.cnt_double_fail_tests += 1 + else: + # A passing call with a teardown error creates separate + # terminal reports, but JUnit XML keeps one testcase + # element for that item (#3850). + self.cnt_double_fail_tests += int( + ( + report.nodeid, + getattr(report, "node", None), + ) + in self.node_reporters + ) reporter = self._opentestcase(report) if report.when == "call": reporter.append_failure(report) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 5a603c05bc8..bfcb981b4b9 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -39,7 +39,10 @@ def __init__(self, pytester: Pytester, schema: xmlschema.XMLSchema) -> None: self.schema = schema def __call__( - self, *args: str | os.PathLike[str], family: str | None = "xunit1" + self, + *args: str | os.PathLike[str], + family: str | None = "xunit1", + suite_name: str = "pytest", ) -> tuple[RunResult, DomDocument]: if family: args = ("-o", "junit_family=" + family, *args) @@ -49,7 +52,11 @@ def __call__( with xml_path.open(encoding="utf-8") as f: self.schema.validate(f) xmldoc = minidom.parse(str(xml_path)) - return result, DomDocument(xmldoc) + doc = DomDocument(xmldoc) + testcase_nodes = doc.find_by_tag("testcase") + test_suite_node = doc.get_first_by_tag("testsuite") + test_suite_node.assert_attr(name=suite_name, tests=len(testcase_nodes)) + return result, doc @pytest.fixture @@ -383,6 +390,7 @@ def test_function(arg): result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.get_first_by_tag("testsuite") + node.assert_attr(errors=1, tests=1) tnode = node.get_first_by_tag("testcase") tnode.assert_attr(classname="test_teardown_error", name="test_function") fnode = tnode.get_first_by_tag("error") @@ -408,7 +416,7 @@ def test_function(arg): result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.get_first_by_tag("testsuite") - node.assert_attr(errors=1, failures=1, tests=1) + node.assert_attr(errors=1, failures=1, tests=2) first, second = dom.find_by_tag("testcase") assert first assert second @@ -1686,7 +1694,7 @@ def test_func(): pass """ ) - result, dom = run_and_parse(family=xunit_family) + result, dom = run_and_parse(family=xunit_family, suite_name=expected) assert result.ret == 0 node = dom.get_first_by_tag("testsuite") node.assert_attr(name=expected)