使用 unittest 测试 argparse - 退出错误
Using unittest to test argparse - exit errors
从 Greg Haskin's answer in this question 开始,我尝试进行单元测试以检查当我向它传递一些 choices
中不存在的参数时 argparse 是否给出了适当的错误。但是,unittest
使用下面的 try/except
语句会产生误报。
此外,当我只使用 with assertRaises
语句进行测试时,argparse
强制系统退出并且程序不再执行任何测试。
我希望能够对此进行测试,但鉴于 argparse
出错时退出,这可能是多余的?
#!/usr/bin/env python3
import argparse
import unittest
class sweep_test_case(unittest.TestCase):
"""Tests that the merParse class works correctly"""
def setUp(self):
self.parser=argparse.ArgumentParser()
self.parser.add_argument(
"-c", "--color",
type=str,
choices=["yellow", "blue"],
required=True)
def test_required_unknown_TE(self):
"""Try to perform sweep on something that isn't an option.
Should return an attribute error if it fails.
This test incorrectly shows that the test passed, even though that must
not be true."""
args = ["--color", "NADA"]
try:
self.assertRaises(argparse.ArgumentError, self.parser.parse_args(args))
except SystemExit:
print("should give a false positive pass")
def test_required_unknown(self):
"""Try to perform sweep on something that isn't an option.
Should return an attribute error if it fails.
This test incorrectly shows that the test passed, even though that must
not be true."""
args = ["--color", "NADA"]
with self.assertRaises(argparse.ArgumentError):
self.parser.parse_args(args)
if __name__ == '__main__':
unittest.main()
错误:
Usage: temp.py [-h] -c {yellow,blue}
temp.py: error: argument -c/--color: invalid choice: 'NADA' (choose from 'yellow', 'blue')
E
usage: temp.py [-h] -c {yellow,blue}
temp.py: error: argument -c/--color: invalid choice: 'NADA' (choose from 'yellow', 'blue')
should give a false positive pass
.
======================================================================
ERROR: test_required_unknown (__main__.sweep_test_case)
Try to perform sweep on something that isn't an option.
----------------------------------------------------------------------
Traceback (most recent call last): #(I deleted some lines)
File "/Users/darrin/anaconda/lib/python3.5/argparse.py", line 2310, in _check_value
raise ArgumentError(action, msg % args)
argparse.ArgumentError: argument -c/--color: invalid choice: 'NADA' (choose from 'yellow', 'blue')
During handling of the above exception, another exception occurred:
Traceback (most recent call last): #(I deleted some lines)
File "/anaconda/lib/python3.5/argparse.py", line 2372, in exit
_sys.exit(status)
SystemExit: 2
如果查看错误日志,您会发现引发了 argparse.ArgumentError
而不是 AttributeError
。您的代码应如下所示:
#!/usr/bin/env python3
import argparse
import unittest
from argparse import ArgumentError
class sweep_test_case(unittest.TestCase):
"""Tests that the merParse class works correctly"""
def setUp(self):
self.parser=argparse.ArgumentParser()
self.parser.add_argument(
"-c", "--color",
type=str,
choices=["yellow", "blue"],
required=True)
def test_required_unknown_TE(self):
"""Try to perform sweep on something that isn't an option.
Should return an attribute error if it fails.
This test incorrectly shows that the test passed, even though that must
not be true."""
args = ["--color", "NADA"]
try:
self.assertRaises(ArgumentError, self.parser.parse_args(args))
except SystemExit:
print("should give a false positive pass")
def test_required_unknown(self):
"""Try to perform sweep on something that isn't an option.
Should return an attribute error if it fails.
This test incorrectly shows that the test passed, even though that must
not be true."""
args = ["--color", "NADA"]
with self.assertRaises(ArgumentError):
self.parser.parse_args(args)
if __name__ == '__main__':
unittest.main()
虽然解析器在解析特定参数期间可能会引发 ArgumentError,但通常会被捕获并传递给 parser.error
和 parse.exit
。结果是打印了用法以及一条错误消息,然后是 sys.exit(2)
.
所以 asssertRaises
不是测试 argparse
中此类错误的好方法。模块的单元测试文件 test/test_argparse.py
有一种精心设计的方法来解决这个问题,包括子类化 ArgumentParser
、重新定义其 error
方法和重定向输出。
parser.parse_known_args
(由 parse_args
调用)以:
结尾
try:
namespace, args = self._parse_known_args(args, namespace)
if hasattr(namespace, _UNRECOGNIZED_ARGS_ATTR):
args.extend(getattr(namespace, _UNRECOGNIZED_ARGS_ATTR))
delattr(namespace, _UNRECOGNIZED_ARGS_ATTR)
return namespace, args
except ArgumentError:
err = _sys.exc_info()[1]
self.error(str(err))
=================
这个测试怎么样(我借鉴了test_argparse.py
的几个想法:
import argparse
import unittest
class ErrorRaisingArgumentParser(argparse.ArgumentParser):
def error(self, message):
#print(message)
raise ValueError(message) # reraise an error
class sweep_test_case(unittest.TestCase):
"""Tests that the Parse class works correctly"""
def setUp(self):
self.parser=ErrorRaisingArgumentParser()
self.parser.add_argument(
"-c", "--color",
type=str,
choices=["yellow", "blue"],
required=True)
def test_required_unknown(self):
"""Try to perform sweep on something that isn't an option.
Should pass"""
args = ["--color", "NADA"]
with self.assertRaises(ValueError) as cm:
self.parser.parse_args(args)
print('msg:',cm.exception)
self.assertIn('invalid choice', str(cm.exception))
if __name__ == '__main__':
unittest.main()
与 运行:
1931:~/mypy$ python3 stack39028204.py
msg: argument -c/--color: invalid choice: 'NADA' (choose from 'yellow', 'blue')
.
----------------------------------------------------------------------
Ran 1 test in 0.002s
OK
如果你查看argparse的源代码,在argparse.py
的1732行附近(我的python版本是3.5.1),有一个ArgumentParser
的方法叫做parse_known_args
。代码是:
# parse the arguments and exit if there are any errors
try:
namespace, args = self._parse_known_args(args, namespace)
if hasattr(namespace, _UNRECOGNIZED_ARGS_ATTR):
args.extend(getattr(namespace, _UNRECOGNIZED_ARGS_ATTR))
delattr(namespace, _UNRECOGNIZED_ARGS_ATTR)
return namespace, args
except ArgumentError:
err = _sys.exc_info()[1]
self.error(str(err))
因此,ArgumentError
将被 argparse
吞噬,并以错误代码退出。如果你想测试这个,我能想到的唯一方法就是模拟 sys.exc_info
.
这里的技巧是捕捉 SystemExit
而不是 ArgumentError
。这是重写的测试以捕获 SystemExit
:
#!/usr/bin/env python3
import argparse
import unittest
class SweepTestCase(unittest.TestCase):
"""Tests that the merParse class works correctly"""
def setUp(self):
self.parser=argparse.ArgumentParser()
self.parser.add_argument(
"-c", "--color",
type=str,
choices=["yellow", "blue"],
required=True)
def test_required_unknown(self):
""" Try to perform sweep on something that isn't an option. """
args = ["--color", "NADA"]
with self.assertRaises(SystemExit):
self.parser.parse_args(args)
if __name__ == '__main__':
unittest.main()
现在可以正确运行,并且测试通过:
$ python scratch.py
usage: scratch.py [-h] -c {yellow,blue}
scratch.py: error: argument -c/--color: invalid choice: 'NADA' (choose from 'yellow', 'blue')
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
但是,您可以看到正在打印用法消息,因此您的测试输出有点混乱。检查用法消息是否包含 "invalid choice".
也可能很好
你可以通过修补 sys.stderr
:
#!/usr/bin/env python3
import argparse
import unittest
from io import StringIO
from unittest.mock import patch
class SweepTestCase(unittest.TestCase):
"""Tests that the merParse class works correctly"""
def setUp(self):
self.parser=argparse.ArgumentParser()
self.parser.add_argument(
"-c", "--color",
type=str,
choices=["yellow", "blue"],
required=True)
@patch('sys.stderr', new_callable=StringIO)
def test_required_unknown(self, mock_stderr):
""" Try to perform sweep on something that isn't an option. """
args = ["--color", "NADA"]
with self.assertRaises(SystemExit):
self.parser.parse_args(args)
self.assertRegexpMatches(mock_stderr.getvalue(), r"invalid choice")
if __name__ == '__main__':
unittest.main()
现在只看到常规检测报告:
$ python scratch.py
.
----------------------------------------------------------------------
Ran 1 test in 0.002s
OK
对于 pytest 用户,这里是不检查消息的等效项。
import argparse
import pytest
def test_required_unknown():
""" Try to perform sweep on something that isn't an option. """
parser=argparse.ArgumentParser()
parser.add_argument(
"-c", "--color",
type=str,
choices=["yellow", "blue"],
required=True)
args = ["--color", "NADA"]
with pytest.raises(SystemExit):
parser.parse_args(args)
Pytest默认抓取stdout/stderr,所以不会污染测试报告
$ pytest scratch.py
================================== test session starts ===================================
platform linux -- Python 3.6.7, pytest-3.5.0, py-1.7.0, pluggy-0.6.0
rootdir: /home/don/.PyCharm2018.3/config/scratches, inifile:
collected 1 item
scratch.py . [100%]
================================ 1 passed in 0.01 seconds ================================
你也可以用pytest检查stdout/stderr内容:
import argparse
import pytest
def test_required_unknown(capsys):
""" Try to perform sweep on something that isn't an option. """
parser=argparse.ArgumentParser()
parser.add_argument(
"-c", "--color",
type=str,
choices=["yellow", "blue"],
required=True)
args = ["--color", "NADA"]
with pytest.raises(SystemExit):
parser.parse_args(args)
stderr = capsys.readouterr().err
assert 'invalid choice' in stderr
像往常一样,我发现 pytest 更容易使用,但您可以让它在任何一个中工作。
我遇到了与 argparse(出口 2)相同错误的类似问题,并更正了它捕获元组的第一个元素 parse_known_args() return,一个 argparse.Namespace 对象.
def test_basics_options_of_parser(self):
parser = w2ptdd.get_parser()
# unpacking tuple
parser_name_space,__ = parser.parse_known_args()
args = vars(parser_name_space)
self.assertFalse(args['unit'])
self.assertFalse(args['functional'])
上面有很多很好的答案,我在 setUp method a parser instance is created inside our test and an argument is added to it, effectively causing the test to be of argparse's implementation. This, of course, could be a valid test/use case but wouldn't necessarily test a script's or application's specific use of argparse 中看到了。
我认为 Yauhen Yakimovich's answer gives good insight into how to make use of argparse 以务实的方式。虽然我还没有完全接受它,但我认为可以通过解析器生成器和覆盖来简化测试方法。
我选择测试我的代码而不是 argparse's implementation. To achieve this we'll want to utilize a factory to create the parser in our code that holds all the argument definitions. This facilitates testing our own parser in setUp。
// my_class.py
import argparse
class MyClass:
def __init__(self):
self.parser = self._create_args_parser()
def _create_args_parser():
parser = argparse.ArgumentParser()
parser.add_argument('--kind',
action='store',
dest='kind',
choices=['type1', 'type2'],
help='kind can be any of: type1, type2')
return parser
在我们的测试中,我们可以生成我们的解析器并针对它进行测试。我们将覆盖错误方法以确保我们不会陷入 argparse's ArgumentError
评估。
import unittest
from my_class import MyClass
class MyClassTest(unittest.TestCase):
def _redefine_parser_error_method(self, message):
raise ValueError
def setUp(self):
parser = MyClass._create_args_parser()
parser.error = self._redefine_parser_error_func
self.parser = parser
def test_override_certificate_kind_arguments(self):
args = ['--kind', 'not-supported']
expected_message = "argument --kind: invalid choice: 'not-supported'.*$"
with self.assertRaisesRegex(ValueError, expected_message):
self.parser.parse_args(args)
这可能不是绝对的最佳答案,但我发现使用我们自己的解析器的参数并通过简单地针对我们知道应该只发生在测试本身中的异常进行测试来测试该部分是很好的。
我知道这是一个老问题,但只是为了扩展@don-kirkby寻找SystemExit
的答案——但不必使用pytest
或patching
– 如果你想对错误消息断言,你可以将测试代码包装在 contextlib.redirect_stderr 中:
import contextlib
from io import StringIO
import unittest
class MyTest(unittest.TestCase):
def test_foo(self):
ioerr = StringIO()
with contextlib.redirect_stderr(ioerr):
with self.assertRaises(SystemExit) as err:
foo('bad')
self.assertEqual(err.exception.code, 2)
self.assertIn("That is a 'bad' thing", ioerr.getvalue())
从 Greg Haskin's answer in this question 开始,我尝试进行单元测试以检查当我向它传递一些 choices
中不存在的参数时 argparse 是否给出了适当的错误。但是,unittest
使用下面的 try/except
语句会产生误报。
此外,当我只使用 with assertRaises
语句进行测试时,argparse
强制系统退出并且程序不再执行任何测试。
我希望能够对此进行测试,但鉴于 argparse
出错时退出,这可能是多余的?
#!/usr/bin/env python3
import argparse
import unittest
class sweep_test_case(unittest.TestCase):
"""Tests that the merParse class works correctly"""
def setUp(self):
self.parser=argparse.ArgumentParser()
self.parser.add_argument(
"-c", "--color",
type=str,
choices=["yellow", "blue"],
required=True)
def test_required_unknown_TE(self):
"""Try to perform sweep on something that isn't an option.
Should return an attribute error if it fails.
This test incorrectly shows that the test passed, even though that must
not be true."""
args = ["--color", "NADA"]
try:
self.assertRaises(argparse.ArgumentError, self.parser.parse_args(args))
except SystemExit:
print("should give a false positive pass")
def test_required_unknown(self):
"""Try to perform sweep on something that isn't an option.
Should return an attribute error if it fails.
This test incorrectly shows that the test passed, even though that must
not be true."""
args = ["--color", "NADA"]
with self.assertRaises(argparse.ArgumentError):
self.parser.parse_args(args)
if __name__ == '__main__':
unittest.main()
错误:
Usage: temp.py [-h] -c {yellow,blue}
temp.py: error: argument -c/--color: invalid choice: 'NADA' (choose from 'yellow', 'blue')
E
usage: temp.py [-h] -c {yellow,blue}
temp.py: error: argument -c/--color: invalid choice: 'NADA' (choose from 'yellow', 'blue')
should give a false positive pass
.
======================================================================
ERROR: test_required_unknown (__main__.sweep_test_case)
Try to perform sweep on something that isn't an option.
----------------------------------------------------------------------
Traceback (most recent call last): #(I deleted some lines)
File "/Users/darrin/anaconda/lib/python3.5/argparse.py", line 2310, in _check_value
raise ArgumentError(action, msg % args)
argparse.ArgumentError: argument -c/--color: invalid choice: 'NADA' (choose from 'yellow', 'blue')
During handling of the above exception, another exception occurred:
Traceback (most recent call last): #(I deleted some lines)
File "/anaconda/lib/python3.5/argparse.py", line 2372, in exit
_sys.exit(status)
SystemExit: 2
如果查看错误日志,您会发现引发了 argparse.ArgumentError
而不是 AttributeError
。您的代码应如下所示:
#!/usr/bin/env python3
import argparse
import unittest
from argparse import ArgumentError
class sweep_test_case(unittest.TestCase):
"""Tests that the merParse class works correctly"""
def setUp(self):
self.parser=argparse.ArgumentParser()
self.parser.add_argument(
"-c", "--color",
type=str,
choices=["yellow", "blue"],
required=True)
def test_required_unknown_TE(self):
"""Try to perform sweep on something that isn't an option.
Should return an attribute error if it fails.
This test incorrectly shows that the test passed, even though that must
not be true."""
args = ["--color", "NADA"]
try:
self.assertRaises(ArgumentError, self.parser.parse_args(args))
except SystemExit:
print("should give a false positive pass")
def test_required_unknown(self):
"""Try to perform sweep on something that isn't an option.
Should return an attribute error if it fails.
This test incorrectly shows that the test passed, even though that must
not be true."""
args = ["--color", "NADA"]
with self.assertRaises(ArgumentError):
self.parser.parse_args(args)
if __name__ == '__main__':
unittest.main()
虽然解析器在解析特定参数期间可能会引发 ArgumentError,但通常会被捕获并传递给 parser.error
和 parse.exit
。结果是打印了用法以及一条错误消息,然后是 sys.exit(2)
.
所以 asssertRaises
不是测试 argparse
中此类错误的好方法。模块的单元测试文件 test/test_argparse.py
有一种精心设计的方法来解决这个问题,包括子类化 ArgumentParser
、重新定义其 error
方法和重定向输出。
parser.parse_known_args
(由 parse_args
调用)以:
try:
namespace, args = self._parse_known_args(args, namespace)
if hasattr(namespace, _UNRECOGNIZED_ARGS_ATTR):
args.extend(getattr(namespace, _UNRECOGNIZED_ARGS_ATTR))
delattr(namespace, _UNRECOGNIZED_ARGS_ATTR)
return namespace, args
except ArgumentError:
err = _sys.exc_info()[1]
self.error(str(err))
=================
这个测试怎么样(我借鉴了test_argparse.py
的几个想法:
import argparse
import unittest
class ErrorRaisingArgumentParser(argparse.ArgumentParser):
def error(self, message):
#print(message)
raise ValueError(message) # reraise an error
class sweep_test_case(unittest.TestCase):
"""Tests that the Parse class works correctly"""
def setUp(self):
self.parser=ErrorRaisingArgumentParser()
self.parser.add_argument(
"-c", "--color",
type=str,
choices=["yellow", "blue"],
required=True)
def test_required_unknown(self):
"""Try to perform sweep on something that isn't an option.
Should pass"""
args = ["--color", "NADA"]
with self.assertRaises(ValueError) as cm:
self.parser.parse_args(args)
print('msg:',cm.exception)
self.assertIn('invalid choice', str(cm.exception))
if __name__ == '__main__':
unittest.main()
与 运行:
1931:~/mypy$ python3 stack39028204.py
msg: argument -c/--color: invalid choice: 'NADA' (choose from 'yellow', 'blue')
.
----------------------------------------------------------------------
Ran 1 test in 0.002s
OK
如果你查看argparse的源代码,在argparse.py
的1732行附近(我的python版本是3.5.1),有一个ArgumentParser
的方法叫做parse_known_args
。代码是:
# parse the arguments and exit if there are any errors
try:
namespace, args = self._parse_known_args(args, namespace)
if hasattr(namespace, _UNRECOGNIZED_ARGS_ATTR):
args.extend(getattr(namespace, _UNRECOGNIZED_ARGS_ATTR))
delattr(namespace, _UNRECOGNIZED_ARGS_ATTR)
return namespace, args
except ArgumentError:
err = _sys.exc_info()[1]
self.error(str(err))
因此,ArgumentError
将被 argparse
吞噬,并以错误代码退出。如果你想测试这个,我能想到的唯一方法就是模拟 sys.exc_info
.
这里的技巧是捕捉 SystemExit
而不是 ArgumentError
。这是重写的测试以捕获 SystemExit
:
#!/usr/bin/env python3
import argparse
import unittest
class SweepTestCase(unittest.TestCase):
"""Tests that the merParse class works correctly"""
def setUp(self):
self.parser=argparse.ArgumentParser()
self.parser.add_argument(
"-c", "--color",
type=str,
choices=["yellow", "blue"],
required=True)
def test_required_unknown(self):
""" Try to perform sweep on something that isn't an option. """
args = ["--color", "NADA"]
with self.assertRaises(SystemExit):
self.parser.parse_args(args)
if __name__ == '__main__':
unittest.main()
现在可以正确运行,并且测试通过:
$ python scratch.py
usage: scratch.py [-h] -c {yellow,blue}
scratch.py: error: argument -c/--color: invalid choice: 'NADA' (choose from 'yellow', 'blue')
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
但是,您可以看到正在打印用法消息,因此您的测试输出有点混乱。检查用法消息是否包含 "invalid choice".
也可能很好你可以通过修补 sys.stderr
:
#!/usr/bin/env python3
import argparse
import unittest
from io import StringIO
from unittest.mock import patch
class SweepTestCase(unittest.TestCase):
"""Tests that the merParse class works correctly"""
def setUp(self):
self.parser=argparse.ArgumentParser()
self.parser.add_argument(
"-c", "--color",
type=str,
choices=["yellow", "blue"],
required=True)
@patch('sys.stderr', new_callable=StringIO)
def test_required_unknown(self, mock_stderr):
""" Try to perform sweep on something that isn't an option. """
args = ["--color", "NADA"]
with self.assertRaises(SystemExit):
self.parser.parse_args(args)
self.assertRegexpMatches(mock_stderr.getvalue(), r"invalid choice")
if __name__ == '__main__':
unittest.main()
现在只看到常规检测报告:
$ python scratch.py
.
----------------------------------------------------------------------
Ran 1 test in 0.002s
OK
对于 pytest 用户,这里是不检查消息的等效项。
import argparse
import pytest
def test_required_unknown():
""" Try to perform sweep on something that isn't an option. """
parser=argparse.ArgumentParser()
parser.add_argument(
"-c", "--color",
type=str,
choices=["yellow", "blue"],
required=True)
args = ["--color", "NADA"]
with pytest.raises(SystemExit):
parser.parse_args(args)
Pytest默认抓取stdout/stderr,所以不会污染测试报告
$ pytest scratch.py
================================== test session starts ===================================
platform linux -- Python 3.6.7, pytest-3.5.0, py-1.7.0, pluggy-0.6.0
rootdir: /home/don/.PyCharm2018.3/config/scratches, inifile:
collected 1 item
scratch.py . [100%]
================================ 1 passed in 0.01 seconds ================================
你也可以用pytest检查stdout/stderr内容:
import argparse
import pytest
def test_required_unknown(capsys):
""" Try to perform sweep on something that isn't an option. """
parser=argparse.ArgumentParser()
parser.add_argument(
"-c", "--color",
type=str,
choices=["yellow", "blue"],
required=True)
args = ["--color", "NADA"]
with pytest.raises(SystemExit):
parser.parse_args(args)
stderr = capsys.readouterr().err
assert 'invalid choice' in stderr
像往常一样,我发现 pytest 更容易使用,但您可以让它在任何一个中工作。
我遇到了与 argparse(出口 2)相同错误的类似问题,并更正了它捕获元组的第一个元素 parse_known_args() return,一个 argparse.Namespace 对象.
def test_basics_options_of_parser(self):
parser = w2ptdd.get_parser()
# unpacking tuple
parser_name_space,__ = parser.parse_known_args()
args = vars(parser_name_space)
self.assertFalse(args['unit'])
self.assertFalse(args['functional'])
上面有很多很好的答案,我在 setUp method a parser instance is created inside our test and an argument is added to it, effectively causing the test to be of argparse's implementation. This, of course, could be a valid test/use case but wouldn't necessarily test a script's or application's specific use of argparse 中看到了。 我认为 Yauhen Yakimovich's answer gives good insight into how to make use of argparse 以务实的方式。虽然我还没有完全接受它,但我认为可以通过解析器生成器和覆盖来简化测试方法。
我选择测试我的代码而不是 argparse's implementation. To achieve this we'll want to utilize a factory to create the parser in our code that holds all the argument definitions. This facilitates testing our own parser in setUp。
// my_class.py
import argparse
class MyClass:
def __init__(self):
self.parser = self._create_args_parser()
def _create_args_parser():
parser = argparse.ArgumentParser()
parser.add_argument('--kind',
action='store',
dest='kind',
choices=['type1', 'type2'],
help='kind can be any of: type1, type2')
return parser
在我们的测试中,我们可以生成我们的解析器并针对它进行测试。我们将覆盖错误方法以确保我们不会陷入 argparse's ArgumentError
评估。
import unittest
from my_class import MyClass
class MyClassTest(unittest.TestCase):
def _redefine_parser_error_method(self, message):
raise ValueError
def setUp(self):
parser = MyClass._create_args_parser()
parser.error = self._redefine_parser_error_func
self.parser = parser
def test_override_certificate_kind_arguments(self):
args = ['--kind', 'not-supported']
expected_message = "argument --kind: invalid choice: 'not-supported'.*$"
with self.assertRaisesRegex(ValueError, expected_message):
self.parser.parse_args(args)
这可能不是绝对的最佳答案,但我发现使用我们自己的解析器的参数并通过简单地针对我们知道应该只发生在测试本身中的异常进行测试来测试该部分是很好的。
我知道这是一个老问题,但只是为了扩展@don-kirkby寻找SystemExit
的答案——但不必使用pytest
或patching
– 如果你想对错误消息断言,你可以将测试代码包装在 contextlib.redirect_stderr 中:
import contextlib
from io import StringIO
import unittest
class MyTest(unittest.TestCase):
def test_foo(self):
ioerr = StringIO()
with contextlib.redirect_stderr(ioerr):
with self.assertRaises(SystemExit) as err:
foo('bad')
self.assertEqual(err.exception.code, 2)
self.assertIn("That is a 'bad' thing", ioerr.getvalue())