unittest-based test runner with Ant/JUnit like XML reporting.
Comprehensive test result collection system that captures detailed test execution information and generates XML reports compatible with JUnit schema and CI/CD systems.
Main result collector that extends unittest's TextTestResult to capture test outcomes, timing information, stdout/stderr, and generate XML reports.
class _XMLTestResult(TextTestResult):
def __init__(self, stream=sys.stderr, descriptions=1, verbosity=1,
elapsed_times=True, properties=None, infoclass=None):
"""
Initialize XML test result collector.
Parameters:
- stream: file-like object for text output
- descriptions: int, description verbosity (0=no, 1=short, 2=long)
- verbosity: int, output verbosity level
- elapsed_times: bool, track test execution timing
- properties: dict or None, JUnit testsuite properties
- infoclass: class or None, custom test info class
"""
def generate_reports(self, test_runner):
"""Generate XML reports using the test runner's configuration."""
def addSuccess(self, test):
"""Record successful test completion."""
def addFailure(self, test, err):
"""Record test failure with exception information."""
def addError(self, test, err):
"""Record test error with exception information."""
def addSkip(self, test, reason):
"""Record skipped test with reason."""
def addSubTest(self, testcase, test, err):
"""Record subtest result (limited support)."""
def addExpectedFailure(self, test, err):
"""Record expected test failure."""
def addUnexpectedSuccess(self, test):
"""Record unexpected test success."""Custom Result Class
import xmlrunner
from xmlrunner.result import _XMLTestResult
class CustomTestResult(_XMLTestResult):
def addSuccess(self, test):
super().addSuccess(test)
print(f"✓ {test.id()}")
runner = xmlrunner.XMLTestRunner(
output='reports',
resultclass=CustomTestResult
)JUnit Properties
import unittest
import xmlrunner
# Add custom properties to testsuite
class TestWithProperties(unittest.TestCase):
def setUp(self):
# Properties can be set on test suite
if not hasattr(self.__class__, 'properties'):
self.__class__.properties = {
'build_number': '123',
'environment': 'staging',
'branch': 'main'
}
def test_example(self):
self.assertTrue(True)
unittest.main(testRunner=xmlrunner.XMLTestRunner(output='reports'))Container for detailed test execution information, used internally by _XMLTestResult to track test outcomes and metadata.
class _TestInfo:
# Test outcome constants
SUCCESS: int = 0
FAILURE: int = 1
ERROR: int = 2
SKIP: int = 3
def __init__(self, test_result, test_method, outcome=SUCCESS, err=None,
subTest=None, filename=None, lineno=None, doc=None):
"""
Initialize test information container.
Parameters:
- test_result: _XMLTestResult instance
- test_method: test method object
- outcome: int, test outcome (SUCCESS/FAILURE/ERROR/SKIP)
- err: tuple or str, exception information
- subTest: subtest object or None
- filename: str or None, test file path
- lineno: int or None, test line number
- doc: str or None, test method docstring
"""
def test_finished(self):
"""Finalize test information after test completion."""
def get_error_info(self):
"""Get formatted exception information."""
def id(self):
"""Get test identifier."""
# Attributes populated during test execution
test_result: _XMLTestResult
outcome: int
elapsed_time: float
timestamp: str
test_name: str
test_id: str
test_description: str
test_exception_name: str
test_exception_message: str
test_exception_info: str
stdout: str
stderr: str
filename: str | None
lineno: int | None
doc: str | NoneThe result system captures stdout/stderr during test execution for inclusion in XML reports.
class _DuplicateWriter:
def __init__(self, first, second):
"""
Dual-output writer that duplicates output to two streams.
Parameters:
- first: primary output stream
- second: secondary stream (typically StringIO for capture)
"""
def write(self, data):
"""Write data to both streams."""
def flush(self):
"""Flush both streams."""
def getvalue(self):
"""Get captured value from secondary stream."""import io
from xmlrunner.result import _DuplicateWriter
# Capture output while still displaying to console
capture = io.StringIO()
dual_writer = _DuplicateWriter(sys.stdout, capture)
# Use dual_writer for test output
# Later retrieve captured content with capture.getvalue()The result system generates XML reports following JUnit schema with support for multiple output formats.
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="test_module.TestClass" tests="3" failures="1" errors="0"
skipped="0" time="0.123" timestamp="2023-12-01T10:30:00">
<properties>
<property name="build_number" value="123"/>
</properties>
<testcase classname="test_module.TestClass" name="test_success"
time="0.001" timestamp="2023-12-01T10:30:00"/>
<testcase classname="test_module.TestClass" name="test_failure"
time="0.002" timestamp="2023-12-01T10:30:01">
<failure type="AssertionError" message="False is not true">
<![CDATA[Traceback (most recent call last):
File "test_module.py", line 10, in test_failure
self.assertTrue(False)
AssertionError: False is not true]]>
</failure>
<system-out><![CDATA[Test output here]]></system-out>
</testcase>
</testsuite>
</testsuites># Format constants for output capture
STDOUT_LINE: str = '\nStdout:\n%s'
STDERR_LINE: str = '\nStderr:\n%s'
def safe_unicode(data, encoding='utf8'):
"""
Convert data to unicode string with only valid XML characters.
Parameters:
- data: input data to convert
- encoding: encoding for byte strings
Returns:
- str: cleaned unicode string
"""
def testcase_name(test_method):
"""
Extract test case name from test method.
Parameters:
- test_method: test method object
Returns:
- str: test case name in format 'module.TestClass'
"""
def resolve_filename(filename):
"""
Make filename relative to current directory when possible.
Parameters:
- filename: str, file path
Returns:
- str: relative or absolute filename
"""The result system works seamlessly with unittest's test discovery:
import unittest
import xmlrunner
# Test discovery with XML reporting
loader = unittest.TestLoader()
suite = loader.discover('tests', pattern='test_*.py')
runner = xmlrunner.XMLTestRunner(output='test-reports')
result = runner.run(suite)
# Access result information
print(f"Tests run: {result.testsRun}")
print(f"Failures: {len(result.failures)}")
print(f"Errors: {len(result.errors)}")
print(f"Skipped: {len(result.skipped)}")Limited support for unittest.TestCase.subTest functionality:
import unittest
import xmlrunner
class TestWithSubTests(unittest.TestCase):
def test_with_subtests(self):
for i in range(3):
with self.subTest(i=i):
self.assertEqual(i % 2, 0) # Will fail for i=1
# Note: SubTest granularity may be lost in XML reports
# due to JUnit schema limitations
unittest.main(testRunner=xmlrunner.XMLTestRunner(output='reports'))Install with Tessl CLI
npx tessl i tessl/pypi-unittest-xml-reporting