1 # -*- coding: utf-8 -*-
3 """unittest-xml-reporting is a PyUnit-based TestRunner that can export test
4 results to XML files that can be consumed by a wide range of tools, such as
5 build systems, IDEs and Continuous Integration servers.
7 This module provides the XMLTestRunner class, which is heavily based on the
8 default TextTestRunner. This makes the XMLTestRunner very simple to use.
10 The script below, adapted from the unittest documentation, shows how to use
11 XMLTestRunner in a very simple way. In fact, the only difference between this
12 script and the original one is the last line:
18 class TestSequenceFunctions(unittest.TestCase):
22 def test_shuffle(self):
23 # make sure the shuffled sequence does not lose any elements
24 random.shuffle(self.seq)
26 self.assertEqual(self.seq, range(10))
28 def test_choice(self):
29 element = random.choice(self.seq)
30 self.assert_(element in self.seq)
32 def test_sample(self):
33 self.assertRaises(ValueError, random.sample, self.seq, 20)
34 for element in random.sample(self.seq, 5):
35 self.assert_(element in self.seq)
37 if __name__ == '__main__':
38 unittest.main(testRunner=xmlrunner.XMLTestRunner(output='test-reports'))
44 from unittest import TestResult, _TextTestResult, TextTestRunner
45 from cStringIO import StringIO
48 class _TestInfo(object):
49 """This class is used to keep useful information about the execution of a
53 # Possible test outcomes
54 (SUCCESS, FAILURE, ERROR) = range(3)
56 def __init__(self, test_result, test_method, outcome=SUCCESS, err=None):
57 "Create a new instance of _TestInfo."
58 self.test_result = test_result
59 self.test_method = test_method
60 self.outcome = outcome
62 self.stdout = test_result.stdout and test_result.stdout.getvalue().strip() or ''
63 self.stderr = test_result.stdout and test_result.stderr.getvalue().strip() or ''
65 def get_elapsed_time(self):
66 """Return the time that shows how long the test method took to
69 return self.test_result.stop_time - self.test_result.start_time
71 def get_description(self):
72 "Return a text representation of the test method."
73 return self.test_result.getDescription(self.test_method)
75 def get_error_info(self):
76 """Return a text representation of an exception thrown by a test
81 if sys.version_info < (2,4):
82 return self.test_result._exc_info_to_string(self.err)
84 return self.test_result._exc_info_to_string(
85 self.err, self.test_method)
88 class _XMLTestResult(_TextTestResult):
89 """A test result class that can express test results in a XML report.
91 Used by XMLTestRunner.
93 def __init__(self, stream=sys.stderr, descriptions=1, verbosity=1, \
95 "Create a new instance of _XMLTestResult."
96 _TextTestResult.__init__(self, stream, descriptions, verbosity)
99 self.elapsed_times = elapsed_times
101 def _prepare_callback(self, test_info, target_list, verbose_str,
103 """Append a _TestInfo to the given target list and sets a callback
104 method to be called by stopTest method.
106 target_list.append(test_info)
108 """This callback prints the test method outcome to the stream,
109 as well as the elapsed time.
112 # Ignore the elapsed times for a more reliable unit testing
113 if not self.elapsed_times:
114 self.start_time = self.stop_time = 0
117 self.stream.writeln('(%.3fs) %s' % \
118 (test_info.get_elapsed_time(), verbose_str))
120 self.stream.write(short_str)
121 self.callback = callback
123 def _patch_standard_output(self):
124 """Replace the stdout and stderr streams with string-based streams
125 in order to capture the tests' output.
127 (self.old_stdout, self.old_stderr) = (sys.stdout, sys.stderr)
128 (sys.stdout, sys.stderr) = (self.stdout, self.stderr) = \
129 (StringIO(), StringIO())
131 def _restore_standard_output(self):
132 "Restore the stdout and stderr streams."
133 (sys.stdout, sys.stderr) = (self.old_stdout, self.old_stderr)
135 def startTest(self, test):
136 "Called before execute each test method."
137 self._patch_standard_output()
138 self.start_time = time.time()
139 TestResult.startTest(self, test)
142 self.stream.write(' ' + self.getDescription(test))
143 self.stream.write(" ... ")
145 def stopTest(self, test):
146 "Called after execute each test method."
147 self._restore_standard_output()
148 _TextTestResult.stopTest(self, test)
149 self.stop_time = time.time()
151 if self.callback and callable(self.callback):
155 def addSuccess(self, test):
156 "Called when a test executes successfully."
157 self._prepare_callback(_TestInfo(self, test),
158 self.successes, 'OK', '.')
160 def addFailure(self, test, err):
161 "Called when a test method fails."
162 self._prepare_callback(_TestInfo(self, test, _TestInfo.FAILURE, err),
163 self.failures, 'FAIL', 'F')
165 def addError(self, test, err):
166 "Called when a test method raises an error."
167 self._prepare_callback(_TestInfo(self, test, _TestInfo.ERROR, err),
168 self.errors, 'ERROR', 'E')
170 def printErrorList(self, flavour, errors):
171 "Write some information about the FAIL or ERROR to the stream."
172 for test_info in errors:
173 self.stream.writeln(self.separator1)
174 self.stream.writeln('%s [%.3fs]: %s' % \
175 (flavour, test_info.get_elapsed_time(), \
176 test_info.get_description()))
177 self.stream.writeln(self.separator2)
178 self.stream.writeln('%s' % test_info.get_error_info())
180 def _get_info_by_testcase(self):
181 """This method organizes test results by TestCase module. This
182 information is used during the report generation, where a XML report
183 will be generated for each TestCase.
185 tests_by_testcase = {}
187 for tests in (self.successes, self.failures, self.errors):
188 for test_info in tests:
189 testcase = type(test_info.test_method)
191 # Ignore module name if it is '__main__'
192 module = testcase.__module__ + '.'
193 if module == '__main__.':
195 testcase_name = module + testcase.__name__
197 if not tests_by_testcase.has_key(testcase_name):
198 tests_by_testcase[testcase_name] = []
199 tests_by_testcase[testcase_name].append(test_info)
201 return tests_by_testcase
203 def _report_testsuite(suite_name, tests, xml_document):
204 "Appends the testsuite section to the XML document."
205 testsuite = xml_document.createElement('testsuite')
206 xml_document.appendChild(testsuite)
208 testsuite.setAttribute('name', str(suite_name))
209 testsuite.setAttribute('tests', str(len(tests)))
211 testsuite.setAttribute('time', '%.3f' % \
212 sum(map(lambda e: e.get_elapsed_time(), tests)))
214 failures = filter(lambda e: e.outcome==_TestInfo.FAILURE, tests)
215 testsuite.setAttribute('failures', str(len(failures)))
217 errors = filter(lambda e: e.outcome==_TestInfo.ERROR, tests)
218 testsuite.setAttribute('errors', str(len(errors)))
222 _report_testsuite = staticmethod(_report_testsuite)
224 def _report_testcase(suite_name, test_result, xml_testsuite, xml_document):
225 "Appends a testcase section to the XML document."
226 testcase = xml_document.createElement('testcase')
227 xml_testsuite.appendChild(testcase)
229 testcase.setAttribute('classname', str(suite_name))
230 testcase.setAttribute('name', test_result.test_method.shortDescription()
231 or getattr(test_result.test_method, '_testMethodName',
232 str(test_result.test_method)))
233 testcase.setAttribute('time', '%.3f' % test_result.get_elapsed_time())
235 if (test_result.outcome != _TestInfo.SUCCESS):
236 elem_name = ('failure', 'error')[test_result.outcome-1]
237 failure = xml_document.createElement(elem_name)
238 testcase.appendChild(failure)
240 failure.setAttribute('type', str(test_result.err[0].__name__))
241 failure.setAttribute('message', str(test_result.err[1]))
243 error_info = test_result.get_error_info()
244 failureText = xml_document.createCDATASection(error_info)
245 failure.appendChild(failureText)
247 _report_testcase = staticmethod(_report_testcase)
249 def _report_output(test_runner, xml_testsuite, xml_document, stdout, stderr):
250 "Appends the system-out and system-err sections to the XML document."
251 systemout = xml_document.createElement('system-out')
252 xml_testsuite.appendChild(systemout)
254 systemout_text = xml_document.createCDATASection(stdout)
255 systemout.appendChild(systemout_text)
257 systemerr = xml_document.createElement('system-err')
258 xml_testsuite.appendChild(systemerr)
260 systemerr_text = xml_document.createCDATASection(stderr)
261 systemerr.appendChild(systemerr_text)
263 _report_output = staticmethod(_report_output)
265 def generate_reports(self, test_runner):
266 "Generates the XML reports to a given XMLTestRunner object."
267 from xml.dom.minidom import Document
268 all_results = self._get_info_by_testcase()
270 if type(test_runner.output) == str and not \
271 os.path.exists(test_runner.output):
272 os.makedirs(test_runner.output)
274 for suite, tests in all_results.items():
278 testsuite = _XMLTestResult._report_testsuite(suite, tests, doc)
279 stdout, stderr = [], []
281 _XMLTestResult._report_testcase(suite, test, testsuite, doc)
283 stdout.extend(['*****************', test.get_description(), test.stdout])
285 stderr.extend(['*****************', test.get_description(), test.stderr])
286 _XMLTestResult._report_output(test_runner, testsuite, doc,
287 '\n'.join(stdout), '\n'.join(stderr))
288 xml_content = doc.toprettyxml(indent='\t')
290 if type(test_runner.output) is str:
291 report_file = open('%s%sTEST-%s.xml' % \
292 (test_runner.output, os.sep, suite), 'w')
294 report_file.write(xml_content)
298 # Assume that test_runner.output is a stream
299 test_runner.output.write(xml_content)
302 class XMLTestRunner(TextTestRunner):
303 """A test runner class that outputs the results in JUnit like XML files.
305 def __init__(self, output='.', stream=sys.stderr, descriptions=True, \
306 verbose=False, elapsed_times=True):
307 "Create a new instance of XMLTestRunner."
308 verbosity = (1, 2)[verbose]
309 TextTestRunner.__init__(self, stream, descriptions, verbosity)
311 self.elapsed_times = elapsed_times
313 def _make_result(self):
314 """Create the TestResult object which will be used to store
315 information about the executed tests.
317 return _XMLTestResult(self.stream, self.descriptions, \
318 self.verbosity, self.elapsed_times)
321 "Run the given test case or test suite."
322 # Prepare the test execution
323 result = self._make_result()
325 # Print a nice header
326 self.stream.writeln()
327 self.stream.writeln('Running tests...')
328 self.stream.writeln(result.separator2)
331 start_time = time.time()
333 stop_time = time.time()
334 time_taken = stop_time - start_time
338 self.stream.writeln(result.separator2)
339 run = result.testsRun
340 self.stream.writeln("Ran %d test%s in %.3fs" %
341 (run, run != 1 and "s" or "", time_taken))
342 self.stream.writeln()
345 if not result.wasSuccessful():
346 self.stream.write("FAILED (")
347 failed, errored = (len(result.failures), len(result.errors))
349 self.stream.write("failures=%d" % failed)
352 self.stream.write(", ")
353 self.stream.write("errors=%d" % errored)
354 self.stream.writeln(")")
356 self.stream.writeln("OK")
359 self.stream.writeln()
360 self.stream.writeln('Generating XML reports...')
361 result.generate_reports(self)