22

How to capture the stdout/stderr of a unittest in a variable? I need to capture the entire output output of the following unit test and send it to SQS. I have tried this:

import unittest, io
from contextlib import redirect_stdout, redirect_stderr


class LogProcessorTests(unittest.TestCase):
    def setUp(self):
        self.var = 'this value'

    def test_var_value(self):
        with io.StringIO() as buf, redirect_stderr(buf):
            print('Running LogProcessor tests...')
            print('Inside test_var_value')
            self.assertEqual(self.var, 'that value')
            print('-----------------------')
            print(buf.getvalue())

However, it doesn't work and the following output appears only on stdout/stderr.

Testing started at 20:32 ...
/Users/myuser/Documents/virtualenvs/app-venv3/bin/python3 "/Applications/PyCharm CE.app/Contents/helpers/pycharm/_jb_unittest_runner.py" --path /Users/myuser/Documents/projects/application/LogProcessor/tests/test_processor_tests.py
Launching unittests with arguments python -m unittest /Users/myuser/Documents/projects/application/LogProcessor/tests/test_processor_tests.py in /Users/myuser/Documents/projects/application/LogProcessor/tests
Running LogProcessor tests...
Inside test_var_value

that value != this value

Expected :this value
Actual   :that value
<Click to see difference>

Traceback (most recent call last):
  File "/Applications/PyCharm CE.app/Contents/helpers/pycharm/teamcity/diff_tools.py", line 32, in _patched_equals
    old(self, first, second, msg)
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 839, in assertEqual
    assertion_func(first, second, msg=msg)
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 1220, in assertMultiLineEqual
    self.fail(self._formatMessage(msg, standardMsg))
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 680, in fail
    raise self.failureException(msg)
AssertionError: 'this value' != 'that value'
- this value
?   ^^
+ that value
?   ^^

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 59, in testPartExecutor
    yield
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 615, in run
    testMethod()
  File "/Users/myuser/Documents/projects/application/LogProcessor/tests/test_processor_tests.py", line 15, in test_var_value
    self.assertEqual(self.var, 'that value')



Ran 1 test in 0.004s

FAILED (failures=1)

Process finished with exit code 1

Assertion failed

Assertion failed

Any idea? Please let me know if more info is needed.

martineau
  • 119,623
  • 25
  • 170
  • 301
Bhushan
  • 18,329
  • 31
  • 104
  • 137

3 Answers3

24

Based on the contextlib.redirect_stdout documentation, this is how you'd redirect stderr or stdout:

import io
import contextlib

f = io.StringIO()
with contextlib.redirect_stderr(f):
    parser = target.parse_args([])
self.assertTrue("error: one of the arguments -p/--propagate -cu/--cleanup is required" in f.getvalue())

You can also combine that with another context manager (like assertRaises) like this:

f = io.StringIO()
with self.assertRaises(SystemExit) as cm, contextlib.redirect_stderr(f):
    parser = target.parse_args([])
self.assertEqual(cm.exception.code, 2)
self.assertTrue("error: one of the arguments -p/--propagate -cu/--cleanup is required" in f.getvalue())
Emilien
  • 2,971
  • 2
  • 22
  • 32
16

If you manually instantiate the test runner (e.g. unittest.TextTestRunner), you can specify the (file) stream it writes to. By default this is sys.stderr, but you can use a StringIO instead. That will capture the output of the unittest itself. The output of your own print-statements will not be captured, but you can use the redirect_stdout context manager for that, using the same StringIO object.

Note that I would recommend to avoid using print-statements, since they will interfere with the output of the unittest framework (your test output will break the output lines of the unittest framework) and it's a bit of a hack to redirect the stdout/stderr streams. A better solution would be to use the logging module instead. You could then add a logging handler that writes all log messages into a StringIO for further processing (in your case: sending to SQS).

Below is example code based on your code using print-statements.

#!/usr/bin/env python3

import contextlib
import io
import unittest


class LogProcessorTests(unittest.TestCase):

    def setUp(self):
        self.var = 'this value'

    def test_var_value(self):
        print('Running LogProcessor tests...')
        print('Inside test_var_value')
        self.assertEqual(self.var, 'that value')
        print('-----------------------')


if __name__ == '__main__':
    # find all tests in this module
    import __main__
    suite = unittest.TestLoader().loadTestsFromModule(__main__)
    with io.StringIO() as buf:
        # run the tests
        with contextlib.redirect_stdout(buf):
            unittest.TextTestRunner(stream=buf).run(suite)
        # process (in this case: print) the results
        print('*** CAPTURED TEXT***:\n%s' % buf.getvalue())

This prints:

*** CAPTURED TEXT***:
Running LogProcessor tests...
Inside test_var_value
F
======================================================================
FAIL: test_var_value (__main__.LogProcessorTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test.py", line 16, in test_var_value
    self.assertEqual(self.var, 'that value')
AssertionError: 'this value' != 'that value'
- this value
?   ^^
+ that value
?   ^^


----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

This confirms all output (from the unittest framework and the testcase itself) were captured in the StringIO object.

wovano
  • 4,543
  • 5
  • 22
  • 49
  • Do you think it is possible to redirect stdout temporarily per TestCase such that one can make some assertions on the content of that buffer? – normanius Nov 30 '19 at 20:28
  • @normanius, yes, I think that would be possible. Did you try it? If you've made an attempt but got stuck, please [ask a new question](https://stackoverflow.com/questions/ask) about it. – wovano Dec 01 '19 at 13:16
  • Thanks. Figured out a way myself already. In case you have a better idea: [find here](https://stackoverflow.com/questions/59201313) the corresponding post. – normanius Dec 05 '19 at 18:36
0

Honestly the easiest way is probably to redirect your output at the OS level--run the test from the command line and > it to a file.

If you are using a build system to execute these, then the build system should be capturing the output for you and you can extract the output from it's build artifacts.

Bill K
  • 62,186
  • 18
  • 105
  • 157
  • Thanks @BillK for the reply. I have to run these tests using a Lambda on-demand. So I don't think I can use `>`. But yeah, it would have been the simplest approach. – Bhushan May 08 '19 at 17:21