0
# Result Collection and XML Generation
1
2
Comprehensive test result collection system that captures detailed test execution information and generates XML reports compatible with JUnit schema and CI/CD systems.
3
4
## Capabilities
5
6
### XMLTestResult Class
7
8
Main result collector that extends unittest's TextTestResult to capture test outcomes, timing information, stdout/stderr, and generate XML reports.
9
10
```python { .api }
11
class _XMLTestResult(TextTestResult):
12
def __init__(self, stream=sys.stderr, descriptions=1, verbosity=1,
13
elapsed_times=True, properties=None, infoclass=None):
14
"""
15
Initialize XML test result collector.
16
17
Parameters:
18
- stream: file-like object for text output
19
- descriptions: int, description verbosity (0=no, 1=short, 2=long)
20
- verbosity: int, output verbosity level
21
- elapsed_times: bool, track test execution timing
22
- properties: dict or None, JUnit testsuite properties
23
- infoclass: class or None, custom test info class
24
"""
25
26
def generate_reports(self, test_runner):
27
"""Generate XML reports using the test runner's configuration."""
28
29
def addSuccess(self, test):
30
"""Record successful test completion."""
31
32
def addFailure(self, test, err):
33
"""Record test failure with exception information."""
34
35
def addError(self, test, err):
36
"""Record test error with exception information."""
37
38
def addSkip(self, test, reason):
39
"""Record skipped test with reason."""
40
41
def addSubTest(self, testcase, test, err):
42
"""Record subtest result (limited support)."""
43
44
def addExpectedFailure(self, test, err):
45
"""Record expected test failure."""
46
47
def addUnexpectedSuccess(self, test):
48
"""Record unexpected test success."""
49
```
50
51
#### Usage Examples
52
53
**Custom Result Class**
54
```python
55
import xmlrunner
56
from xmlrunner.result import _XMLTestResult
57
58
class CustomTestResult(_XMLTestResult):
59
def addSuccess(self, test):
60
super().addSuccess(test)
61
print(f"✓ {test.id()}")
62
63
runner = xmlrunner.XMLTestRunner(
64
output='reports',
65
resultclass=CustomTestResult
66
)
67
```
68
69
**JUnit Properties**
70
```python
71
import unittest
72
import xmlrunner
73
74
# Add custom properties to testsuite
75
class TestWithProperties(unittest.TestCase):
76
def setUp(self):
77
# Properties can be set on test suite
78
if not hasattr(self.__class__, 'properties'):
79
self.__class__.properties = {
80
'build_number': '123',
81
'environment': 'staging',
82
'branch': 'main'
83
}
84
85
def test_example(self):
86
self.assertTrue(True)
87
88
unittest.main(testRunner=xmlrunner.XMLTestRunner(output='reports'))
89
```
90
91
### TestInfo Class
92
93
Container for detailed test execution information, used internally by _XMLTestResult to track test outcomes and metadata.
94
95
```python { .api }
96
class _TestInfo:
97
# Test outcome constants
98
SUCCESS: int = 0
99
FAILURE: int = 1
100
ERROR: int = 2
101
SKIP: int = 3
102
103
def __init__(self, test_result, test_method, outcome=SUCCESS, err=None,
104
subTest=None, filename=None, lineno=None, doc=None):
105
"""
106
Initialize test information container.
107
108
Parameters:
109
- test_result: _XMLTestResult instance
110
- test_method: test method object
111
- outcome: int, test outcome (SUCCESS/FAILURE/ERROR/SKIP)
112
- err: tuple or str, exception information
113
- subTest: subtest object or None
114
- filename: str or None, test file path
115
- lineno: int or None, test line number
116
- doc: str or None, test method docstring
117
"""
118
119
def test_finished(self):
120
"""Finalize test information after test completion."""
121
122
def get_error_info(self):
123
"""Get formatted exception information."""
124
125
def id(self):
126
"""Get test identifier."""
127
128
# Attributes populated during test execution
129
test_result: _XMLTestResult
130
outcome: int
131
elapsed_time: float
132
timestamp: str
133
test_name: str
134
test_id: str
135
test_description: str
136
test_exception_name: str
137
test_exception_message: str
138
test_exception_info: str
139
stdout: str
140
stderr: str
141
filename: str | None
142
lineno: int | None
143
doc: str | None
144
```
145
146
### Output Capture
147
148
The result system captures stdout/stderr during test execution for inclusion in XML reports.
149
150
```python { .api }
151
class _DuplicateWriter:
152
def __init__(self, first, second):
153
"""
154
Dual-output writer that duplicates output to two streams.
155
156
Parameters:
157
- first: primary output stream
158
- second: secondary stream (typically StringIO for capture)
159
"""
160
161
def write(self, data):
162
"""Write data to both streams."""
163
164
def flush(self):
165
"""Flush both streams."""
166
167
def getvalue(self):
168
"""Get captured value from secondary stream."""
169
```
170
171
#### Usage Example
172
173
```python
174
import io
175
from xmlrunner.result import _DuplicateWriter
176
177
# Capture output while still displaying to console
178
capture = io.StringIO()
179
dual_writer = _DuplicateWriter(sys.stdout, capture)
180
181
# Use dual_writer for test output
182
# Later retrieve captured content with capture.getvalue()
183
```
184
185
### XML Report Generation
186
187
The result system generates XML reports following JUnit schema with support for multiple output formats.
188
189
#### XML Structure
190
191
```xml
192
<?xml version="1.0" encoding="UTF-8"?>
193
<testsuites>
194
<testsuite name="test_module.TestClass" tests="3" failures="1" errors="0"
195
skipped="0" time="0.123" timestamp="2023-12-01T10:30:00">
196
<properties>
197
<property name="build_number" value="123"/>
198
</properties>
199
<testcase classname="test_module.TestClass" name="test_success"
200
time="0.001" timestamp="2023-12-01T10:30:00"/>
201
<testcase classname="test_module.TestClass" name="test_failure"
202
time="0.002" timestamp="2023-12-01T10:30:01">
203
<failure type="AssertionError" message="False is not true">
204
<![CDATA[Traceback (most recent call last):
205
File "test_module.py", line 10, in test_failure
206
self.assertTrue(False)
207
AssertionError: False is not true]]>
208
</failure>
209
<system-out><![CDATA[Test output here]]></system-out>
210
</testcase>
211
</testsuite>
212
</testsuites>
213
```
214
215
### Constants and Utility Functions
216
217
```python { .api }
218
# Format constants for output capture
219
STDOUT_LINE: str = '\nStdout:\n%s'
220
STDERR_LINE: str = '\nStderr:\n%s'
221
222
def safe_unicode(data, encoding='utf8'):
223
"""
224
Convert data to unicode string with only valid XML characters.
225
226
Parameters:
227
- data: input data to convert
228
- encoding: encoding for byte strings
229
230
Returns:
231
- str: cleaned unicode string
232
"""
233
234
def testcase_name(test_method):
235
"""
236
Extract test case name from test method.
237
238
Parameters:
239
- test_method: test method object
240
241
Returns:
242
- str: test case name in format 'module.TestClass'
243
"""
244
245
def resolve_filename(filename):
246
"""
247
Make filename relative to current directory when possible.
248
249
Parameters:
250
- filename: str, file path
251
252
Returns:
253
- str: relative or absolute filename
254
"""
255
```
256
257
### Integration with Test Discovery
258
259
The result system works seamlessly with unittest's test discovery:
260
261
```python
262
import unittest
263
import xmlrunner
264
265
# Test discovery with XML reporting
266
loader = unittest.TestLoader()
267
suite = loader.discover('tests', pattern='test_*.py')
268
269
runner = xmlrunner.XMLTestRunner(output='test-reports')
270
result = runner.run(suite)
271
272
# Access result information
273
print(f"Tests run: {result.testsRun}")
274
print(f"Failures: {len(result.failures)}")
275
print(f"Errors: {len(result.errors)}")
276
print(f"Skipped: {len(result.skipped)}")
277
```
278
279
### SubTest Support
280
281
Limited support for unittest.TestCase.subTest functionality:
282
283
```python
284
import unittest
285
import xmlrunner
286
287
class TestWithSubTests(unittest.TestCase):
288
def test_with_subtests(self):
289
for i in range(3):
290
with self.subTest(i=i):
291
self.assertEqual(i % 2, 0) # Will fail for i=1
292
293
# Note: SubTest granularity may be lost in XML reports
294
# due to JUnit schema limitations
295
unittest.main(testRunner=xmlrunner.XMLTestRunner(output='reports'))
296
```