How to capture the stdout/stderr of a unittest in a variable? - python

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.

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())

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.

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.

Related

assertRaises(AttributeError, ...) not working. Module 'myModule' has no attribute 'main' (Python Unit Test)

I'm new to Python. My first unit test doesn't work.
Here is my telegram.py:
#!/usr/bin/python3
import socket
import sys
import urllib.parse
import certifi
import pycurl
from io import BytesIO
# telegram API key and chat id
TELEGRAM_API_KEY = 'xxx'
TELEGRAM_CHAT_ID = 'xxx'
DEBUG_MODE: bool = False
# stuff to run always here such as class/def
def main(msg):
if not msg:
print("No message to be sent has been passed.")
exit(1)
def debug(debug_type, debug_msg):
if DEBUG_MODE:
print(f"debug({debug_type}): {debug_msg}")
def send_message(message):
print("sending telegram...")
c = pycurl.Curl()
if DEBUG_MODE:
c.setopt(pycurl.VERBOSE, 1)
c.setopt(pycurl.DEBUGFUNCTION, debug)
params = {
'chat_id': TELEGRAM_CHAT_ID,
'text': message
}
telegram_url = f"https://api.telegram.org/bot{TELEGRAM_API_KEY}/sendMessage?" + urllib.parse.urlencode(params)
c.setopt(pycurl.CAINFO, certifi.where())
storage = BytesIO()
c.setopt(c.WRITEDATA, storage)
c.setopt(c.URL, telegram_url)
c.perform()
c.close()
print(storage.getvalue())
send_message(f"{socket.gethostname()}: {msg}")
if __name__ == "__main__":
# stuff only to run when not called via 'import' here
if len(sys.argv) > 1:
main(sys.argv[1])
else:
print("No message to be sent has been passed.")
exit(1)
I want to test this script. I can call this script directly from shell with command line argument oder can call this script in another python script like telegram.main("Test Message").
My unit test doesn't work. I expect an AttributeError because I don't give the argument for telegram.main().
Here is the unit test:
import unittest
import telegram
import subprocess
class TelegramTestCase(unittest.TestCase):
"""Tests for 'telegram.py'"""
def test_empty_telegram(self):
"""call telegram directly without an argument"""
self.assertRaises(AttributeError, telegram.main())
def test_string_telegram(self):
"""call telegram directly with correct argument"""
telegram.main("TästString...123*ß´´OK")
self.assertTrue(True)
if __name__ == '__main__':
unittest.main()
The result of the first test case is:
Error Traceback (most recent call last): File
"C:\Users\XXX\AppData\Local\Programs\Python\Python38-32\lib\unittest\case.py",
line 60, in testPartExecutor
yield File "C:\Users\XXX\AppData\Local\Programs\Python\Python38-32\lib\unittest\case.py",
line 676, in run
self._callTestMethod(testMethod) File "C:\Users\XXX\AppData\Local\Programs\Python\Python38-32\lib\unittest\case.py",
line 633, in _callTestMethod
method() File "G:\Repositories\python\unittests\telegram.py", line 11, in test_empty_telegram
self.assertRaises(AttributeError, telegram.main()) AttributeError: module 'telegram' has no attribute 'main'
Ran 1 test in 0.003s
FAILED (errors=1)
Process finished with exit code 1
What is the problem here? I think telegram.main() and telegram.main("Test") can't be found but why?
I want to test telegram.main() and I am asserting an AttributeError because I don't give an argument so the test should be passed.
Thank you.
Update:
I changed the first test method to
def test_empty_telegram(self):
"""call telegram directly without an argument"""
with self.assertRaises(AttributeError):
telegram.main()
and it works. But the following test methods have the same errors.
def test_string_telegram(self):
"""call telegram directly with correct argument"""
with self.assertRaises(SystemExit) as cm:
telegram.main("TästString...123*ß´´OK")
self.assertEqual(cm.exception.code, 0)
Output:
Testing started at 22:43 ...
C:\Users\XXX\AppData\Local\Programs\Python\Python38-32\python.exe
C:\Users\XXX\AppData\Roaming\JetBrains\IntelliJIdea2020.2\plugins\python\helpers\pycharm_jb_unittest_runner.py
--target telegram.TelegramTestCase.test_string_telegram Launching unittests with arguments python -m unittest
telegram.TelegramTestCase.test_string_telegram in
G:\Repositories\python\unittests
Error Traceback (most recent call last): File
"C:\Users\XXX\AppData\Local\Programs\Python\Python38-32\lib\unittest\case.py",
line 60, in testPartExecutor
yield File "C:\Users\XXX\AppData\Local\Programs\Python\Python38-32\lib\unittest\case.py",
line 676, in run
self._callTestMethod(testMethod) File "C:\Users\XXX\AppData\Local\Programs\Python\Python38-32\lib\unittest\case.py",
line 633, in _callTestMethod
method() File "G:\Repositories\python\unittests\telegram.py", line 17, in test_string_telegram
telegram.main("TästString...123*ß´´OK") AttributeError: module 'telegram' has no attribute 'main'
Ran 1 test in 0.003s
FAILED (errors=1)
Process finished with exit code 1
Assertion failed
Assertion failed
Assertion failed
Update 2
The problem was that I named my module telegram.py. The test file has got the same name in a subfolder. The problem is solved by renaming the test file. Thanks to the helping commentators!
As assertRaises needs a function object as second argument, not the call result, just add lambda: in front of telegram.main() like this:
def test_empty_telegram(self):
self.assertRaises(AttributeError, lambda: telegram.main())

How to replace pytest output for test completely with a custom string?

Scenario
I am writing a package that requires me to externally call pytest from within a pytest run via subprocess. So obviously the output captured from the subprocess is exactly the error I want to display as it has all the nice formatting and info that pytest provides. Unfortunately, currently the main pytest call just shows internal code of my wrapper instead of the nice subprocess output which, after I print it, only is shown in the captured stdout section of pytest.
I would like to format the output for failures and errors as if the code was called directly and hide that a subprocess call was made. Hence, I basically want to completely replace the output for one test-function with a different string. Is this possible?
MWE
Let's look at a MWE of a simple wrapped function (not doing anything useful, but the shortest MWE I could think of):
import functools
from subprocess import Popen, PIPE
import sys
def call_something(func):
#functools.wraps(func)
def wrapper(*args, **kwargs):
# for the sake of simplicity, a dummy call
p = Popen([sys.executable, '-c', 'import numby'], stderr=PIPE, stdout=PIPE)
# let's imagine this is the perfect string output we want
# to print instead of the actual output generated from...
error = p.communicate()[1].decode("utf-8")
if p.returncode != 0:
# ... this line
raise AssertionError(f'{error}')
return wrapper
#call_something
def test_something():
assert 1 == 2
As you can see I have my test_something() function which in the subprocess will fail.
Current output
If I run pytest with this file I get:
================================== FAILURES ===================================
_______________________________ test_something ________________________________
func = <function test_something at 0x000001EA414F1400>, args = (), kwargs = {}
p = <subprocess.Popen object at 0x000001EA414A67B8>
error = 'Traceback (most recent call last):\r\n File "<string>", line 1, in <module>\r\nModuleNotFoundError: No module named \'numby\'\r\n'
def wrapper(*args, **kwargs):
# for the sake of simplicity, a dummy call
p = Popen([sys.executable, '-c', 'import numby'], stderr=PIPE, stdout=PIPE)
# let's imagine this is the perfect string output we want
# to print instead of the actual output generated from...
error = p.communicate()[1].decode("utf-8")
if p.returncode != 0:
# ... this line
> raise AssertionError(f'{error}')
E AssertionError: Traceback (most recent call last):
E File "<string>", line 1, in <module>
E ModuleNotFoundError: No module named 'numby'
test_me.py:18: AssertionError
========================== 1 failed in 0.18 seconds ===========================
Obviously, I don't want to show the details of the wrapper function. Instead
Desired output
I would like to show what happens in the subprocess. So it should look like this (or similar).
================================== FAILURES ===================================
_______________________________ test_something ________________________________
<string captured from subprocess>
========================== 1 failed in 0.11 seconds ===========================
So my question in smaller bits:
Question in short
How to replace the pytest output for test with a custom string
from my wrapper?
When raising errors, you can use the from syntax to suppress or change how exceptions are chained. For example, consider the following:
try:
a / b
except ZeroDivisionError:
raise ValueError('Invalid b value')
This will show a ZeroDivisionError, if b == 0 followed by a ValueError, but you may want to suppress the ZeroDivisionError, because the only relevant part is the ValueError. So you'd instead write:
try:
a / b
except ZeroDivisionError:
raise ValueError('Invalid b value') from None
This will only show the ValueError, telling you that b was wrong
There are other things you can do with the from syntax which will be relevant here, see this thread for details.
I do something very similar to what you're trying by suppressing weird pipe errors and instead raising ModuleNotFoundError:
# Test if <module> is callable
try:
sp.run('<run some module>', shell=True, check=True, stdout=sp.PIPE)
except sp.CalledProcessError as exc:
raise ModuleNotFoundError(
'<some module> not working. Please ensure it is installed and can be called with the command: <command>'
) from exc
Note the use of the from exc here.

How to find a spurious print statement?

I'm debugging a large Python codebase. Somewhere, a piece of code is printing {} to console, presumably this is some old debugging code that's been left in by accident.
As this is the only console output that doesn't go through logger, is there any way I can find the culprit? Perhaps by redefining what the print statement does, so I can cause an exception?
Try to redirect sys.stdout to custom stream handler (see Redirect stdout to a file in Python?), where you can override write() method.
Try something like this:
import io
import sys
import traceback
class TestableIO(io.BytesIO):
def __init__(self, old_stream, initial_bytes=None):
super(TestableIO, self).__init__(initial_bytes)
self.old_stream = old_stream
def write(self, bytes):
if 'bb' in bytes:
traceback.print_stack(file=self.old_stream)
self.old_stream.write(bytes)
sys.stdout = TestableIO(sys.stdout)
sys.stderr = TestableIO(sys.stderr)
print('aa')
print('bb')
print('cc')
Then you will get nice traceback:
λ python test.py
aa
File "test.py", line 22, in <module>
print('bb')
File "test.py", line 14, in write
traceback.print_stack(file=self.old_stream)
bb
cc

Log stack trace for python warning

A package that I'm using in my python program is throwing a warning that I'd like to understand the exact cause of. I've set logging.captureWarning(True) and am capturing the warning in my logging, but still have no idea where it is coming from. How do I also log the stack trace so I can see where in my code the warning is coming from? Do I use traceback?
I've ended up going with the below:
import warnings
import traceback
_formatwarning = warnings.formatwarning
def formatwarning_tb(*args, **kwargs):
s = _formatwarning(*args, **kwargs)
tb = traceback.format_stack()
s += ''.join(tb[:-1])
return s
warnings.formatwarning = formatwarning_tb
logging.captureWarnings(True)
It's a little hackish, but you can monkeypatch the warnings.warn method to this:
import traceback
import warnings
def g():
warnings.warn("foo", Warning)
def f():
g()
warnings.warn("bar", Warning)
_old_warn = warnings.warn
def warn(*args, **kwargs):
tb = traceback.extract_stack()
_old_warn(*args, **kwargs)
print("".join(traceback.format_list(tb)[:-1]))
warnings.warn = warn
f()
print("DONE")
This is the output:
/tmp/test.py:14: Warning: foo
_old_warn(*args, **kwargs)
File "/tmp/test.py", line 17, in <module>
f()
File "/tmp/test.py", line 8, in f
g()
File "/tmp/test.py", line 5, in g
warnings.warn("foo", Warning)
/tmp/test.py:14: Warning: bar
_old_warn(*args, **kwargs)
File "/tmp/test.py", line 17, in <module>
f()
File "/tmp/test.py", line 9, in f
warnings.warn("bar", Warning)
DONE
See that calling the original warnings.warn function does not report the line you'd want, bu the stack trace is indeed correct (you could print the warning message yourself).
If you do not know what data/instruction is causing the warning throw, you can use tools like the standard Python Debugger.
The documentation is really good and detailed, but some quickly examples that may help should be:
Without modifying source code: invoking the debbugger as script:
$ python -m pdb myscript.py
Modifying source code: you can make use of calls to pdb.set_trace(), that work like breakpoints; For example, consider I have the following example code:
x = 2
x = x * 10 * 100
y = x + 3 + y
return y
And I would like to know what value does x and y have before the return, or what does the stack contains, I would add the following line between those statements:
pdb.set_trace()
And I will be promted to the (Pdb) prompt, that will allow you to go through the code line by line. Useful commands for the (Pdb) prompt are:
n: executes the next statement.
q: quits the whole program.
c: quits the (Pdb) prompt and stops debugging.
p varname: prints the value of varname
As you do not provide more information, I do not know if that should be enough, but I think that at least, it may be a good start.
BONUS EDIT
Based on this answer, I have found there is a nice and friendly GUI debugging tool, that you can simply install by:
$ pip install pudb
And run the debugger with your script with:
$ python -m pudb.run myscript.py
EDIT: Adding the postmortem debugging
If we do not even know if the code is going to crash or not, we can enter in postmortem debugging if there has been a crash. From the Pbd documentation:
The typical usage to inspect a crashed program is:
>>> import pdb
>>> import mymodule
>>> mymodule.test()
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "./mymodule.py", line 4, in test
test2()
File "./mymodule.py", line 3, in test2
print spam
NameError: spam
>>> pdb.pm()
> ./mymodule.py(3)test2()
-> print spam
(Pdb)
As postmortem looks at sys.last_traceback, to enter only if there is a traceback (and so on, a warning or crash):
if sys.last_traceback:
pdb.pm()
You can turn warnings into exceptions, which means you will get a stack trace automatically:
warnings.filterwarnings("error")
See https://docs.python.org/3.4/library/warnings.html#the-warnings-filter
If it was me, I'd go with #Lluís Vilanova's quick & dirty hack, just to find something. But if that's not an option...
If you really want a "logging" solution, you could try something like this (fully working source).
Basic steps are:
Create a custom logging.Formatter subclass that includes the current stack where the logging record is formatted
Use that formatter on the class of the warning
The meat of the code is the custom formatter:
class Formatter(logging.Formatter):
def format(self, record):
record.stack_info = ''.join(traceback.format_stack())
return super().format(record)
Per the docs:
New in version 3.2: The stack_info parameter was added.
For python 3.2 and above, using the optional stack_info keyword argument is the easiest way to get stack trace info along with the log message.
In the example below, "Server.py" is using "lib2.py", which is in turn using "lib.py".
On enabling the stack_info argument the complete trace back is logged along with every logging.log() call. This works the same with logging.info() and other convenience methods as well.
Usage :-
logging.log(DEBUG, "RWL [{}] : acquire_read()".format(self._ownerName), stack_info=True)
Output :-
2018-10-06 10:59:55,726|DEBUG|MainThread|lib.py|acquire_read|RWL [Cache] : acquire_read()
Stack (most recent call last):
File "./Server.py", line 41, in <module>
logging.info("Found {} requests for simulation".format(simdata.count()))
File "<Path>\lib2.py", line 199, in count
with basics.ReadRWLock(self.cacheLock):
File "<Path>\lib.py", line 89, in __enter__
self.rwLock.acquire_read()
File "<Path>\lib.py", line 34, in acquire_read
logging.log(DEBUG, "RWL [{}] : acquire_read()".format(self._ownerName), stack_info=True)

nose plugin for expected-failures

Is there an existing plugin which could be used like:
#nose.plugins.expectedfailure
def not_done_yet():
a = Thingamajig().fancynewthing()
assert a == "example"
If the test fails, it would appear like a skipped test:
$ nosetests
...S..
..but if it unexpected passes, it would appear similarly to a failure, maybe like:
=================================
UNEXPECTED PASS: not_done_yet
---------------------------------
-- >> begin captured stdout << --
Things and etc
...
Kind of like SkipTest, but not implemented as an exception which prevents the test from running.
Only thing I can find is this ticket about supporting the unittest2 expectedFailure decorator (although I'd rather not use unittest2, even if nose supported it)
I don't know about a nose plugin, but you could easily write your own decorator to do that. Here's a simple implementation:
import functools
import nose
def expected_failure(test):
#functools.wraps(test)
def inner(*args, **kwargs):
try:
test(*args, **kwargs)
except Exception:
raise nose.SkipTest
else:
raise AssertionError('Failure expected')
return inner
If I run these tests:
#expected_failure
def test_not_implemented():
assert False
#expected_failure
def test_unexpected_success():
assert True
I get the following output from nose:
tests.test.test_not_implemented ... SKIP
tests.test.test_unexpected_success ... FAIL
======================================================================
FAIL: tests.test.test_unexpected_success
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Python32\lib\site-packages\nose-1.1.2-py3.2.egg\nose\case.py", line 198, in runTest
self.test(*self.arg)
File "G:\Projects\Programming\dt-tools\new-sanbi\tests\test.py", line 16, in inner
raise AssertionError('Failure expected')
AssertionError: Failure expected
----------------------------------------------------------------------
Ran 2 tests in 0.016s
FAILED (failures=1)
Forgive me if I've misunderstood, but isn't the behavior you desire provided by core python's unittest library with the expectedFailure decorator, which is—by extension—compatible with nose?
For an example of use see the docs and a post about its implementation.
You can do it with one of two ways:
nose.tools.raises decorator
from nose.tools import raises
#raises(TypeError)
def test_raises_type_error():
raise TypeError("This test passes")
nose.tools.assert_raises
from nose.tools import assert_raises
def test_raises_type_error():
with assert_raises(TypeError):
raise TypeError("This test passes")
Tests will fail if exception is not raised.
Yup, I know, asked 3 years ago :)

Categories

Resources