unittest-based test runner with Ant/JUnit like XML reporting.
Low-level utilities for building XML test reports with proper JUnit schema compliance. Provides context management, counter tracking, and CDATA section handling for generating well-formed XML documents.
Main XML document builder that encapsulates rules for creating JUnit-compatible XML test reports with proper hierarchy and formatting.
class TestXMLBuilder:
def __init__(self):
"""Initialize XML document builder with empty document."""
def begin_context(self, tag, name):
"""
Begin new XML context (testsuites, testsuite, or testcase).
Parameters:
- tag: str, XML tag name
- name: str, context name attribute
"""
def end_context(self):
"""
End current context and append to parent.
Returns:
- bool: True if context was ended, False if no context
"""
def current_context(self):
"""
Get current XML context.
Returns:
- TestXMLContext: current context or None
"""
def context_tag(self):
"""
Get tag name of current context.
Returns:
- str: current context tag name
"""
def append(self, tag, content, **kwargs):
"""
Append XML element with attributes to current context.
Parameters:
- tag: str, XML tag name
- content: str, element content
- **kwargs: element attributes
Returns:
- Element: created XML element
"""
def append_cdata_section(self, tag, content):
"""
Append element with CDATA content to current context.
Parameters:
- tag: str, XML tag name
- content: str, CDATA content
Returns:
- Element: created XML element
"""
def increment_counter(self, counter_name):
"""
Increment counter in current context and all parents.
Parameters:
- counter_name: str, counter name ('tests', 'errors', 'failures', 'skipped')
"""
def finish(self):
"""
End all open contexts and return formatted XML document.
Returns:
- bytes: pretty-printed XML document
"""Basic XML Report Construction
from xmlrunner.builder import TestXMLBuilder
builder = TestXMLBuilder()
# Create testsuites root
builder.begin_context('testsuites', 'all_tests')
# Create testsuite
builder.begin_context('testsuite', 'test_module.TestClass')
# Add testcase
builder.begin_context('testcase', 'test_example')
builder.append('success', '', message='Test passed')
builder.increment_counter('tests')
builder.end_context() # End testcase
builder.end_context() # End testsuite
builder.end_context() # End testsuites
# Generate XML
xml_content = builder.finish()
print(xml_content.decode('utf-8'))Error Reporting with CDATA
builder = TestXMLBuilder()
builder.begin_context('testsuites', 'all_tests')
builder.begin_context('testsuite', 'test_module.TestClass')
builder.begin_context('testcase', 'test_failure')
# Add failure with traceback
failure_info = """Traceback (most recent call last):
File "test.py", line 10, in test_failure
self.assertTrue(False)
AssertionError: False is not true"""
builder.append('failure', '', type='AssertionError', message='False is not true')
builder.append_cdata_section('failure', failure_info)
builder.increment_counter('tests')
builder.increment_counter('failures')
xml_content = builder.finish()Represents XML document hierarchy context with automatic counter tracking and time measurement.
class TestXMLContext:
def __init__(self, xml_doc, parent_context=None):
"""
Initialize XML context.
Parameters:
- xml_doc: Document, XML document instance
- parent_context: TestXMLContext or None, parent context
"""
def begin(self, tag, name):
"""
Begin context by creating XML element.
Parameters:
- tag: str, XML tag name
- name: str, name attribute value
"""
def end(self):
"""
End context and set timing/counter attributes.
Returns:
- Element: completed XML element
"""
def element_tag(self):
"""
Get tag name of this context's element.
Returns:
- str: tag name
"""
def increment_counter(self, counter_name):
"""
Increment counter if valid for this context type.
Parameters:
- counter_name: str, counter name
"""
def elapsed_time(self):
"""
Get formatted elapsed time for this context.
Returns:
- str: elapsed time in seconds (3 decimal places)
"""
def timestamp(self):
"""
Get ISO-8601 formatted timestamp for context end.
Returns:
- str: timestamp string
"""
# Attributes
xml_doc: Document
parent: TestXMLContext | None
element: Element
counters: dict[str, int]Different XML elements support different counters:
Functions for cleaning and processing text content for XML compatibility.
def replace_nontext(text, replacement='\uFFFD'):
"""
Replace invalid XML characters in text.
Parameters:
- text: str, input text
- replacement: str, replacement character
Returns:
- str: cleaned text with invalid XML characters replaced
"""
# Constants
UTF8: str = 'UTF-8' # Default encoding for XML documents
INVALID_XML_1_0_UNICODE_RE: Pattern # Regex for invalid XML 1.0 charactersText Cleaning
from xmlrunner.builder import replace_nontext
# Clean problematic characters from test output
raw_output = "Test output with \x00 null character"
clean_output = replace_nontext(raw_output)
# Result: "Test output with � null character"
# Use in XML building
builder.append_cdata_section('system-out', clean_output)The builder properly handles CDATA sections, including splitting content that contains the CDATA end marker.
# Content with CDATA end marker is automatically split
problematic_content = "Some content ]]> with end marker ]]> inside"
builder.append_cdata_section('system-out', problematic_content)
# Results in multiple CDATA sections:
# <system-out><![CDATA[Some content ]]]]><![CDATA[> with end marker ]]]]><![CDATA[> inside]]></system-out>The builder generates XML that complies with JUnit schema requirements:
The XML builder is optimized for typical test report sizes:
The builder is used internally by _XMLTestResult but can be used independently:
from xmlrunner.builder import TestXMLBuilder
from xml.dom.minidom import Document
# Direct usage for custom XML generation
builder = TestXMLBuilder()
# Build custom structure
builder.begin_context('testsuites', 'custom_run')
builder.begin_context('testsuite', 'my_tests')
for test_name, result in test_results.items():
builder.begin_context('testcase', test_name)
if result['passed']:
builder.increment_counter('tests')
else:
builder.append('failure', '', type='CustomError', message=result['error'])
builder.increment_counter('tests')
builder.increment_counter('failures')
builder.end_context()
xml_bytes = builder.finish()Install with Tessl CLI
npx tessl i tessl/pypi-unittest-xml-reporting