离开Greg Haskin's answer in this question时,我尝试进行单元测试,检查argparse在传递choices
中不存在的一些args时是否给出了相应的错误。但是,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
答案 0 :(得分:4)
这里的技巧是捕获{
"configuration": {
"load": {
"destinationTable": {
"projectId": "<var_destinationProject>",
"datasetId": "<var_destinationDataset>",
"tableId": "<var_destinationTable>_<var_date>"
},
"sourceUris": [
"gs://<var_bucket>/<var_name>"
],
"writeDisposition": "<var_writeDeposition>",
"fieldDelimiter": "|",
"skipLeadingRows": 1,
"allowQuotedNewLines": true,
"allowJaggedRows": true,
"autodetect": true
}
},
"jobReference": {
"jobId": "<var_get_job_id_return>"
}
}
而不是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
来做到这一点:
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()
对于pytest用户,这是不检查消息的等效项。
$ python scratch.py
.
----------------------------------------------------------------------
Ran 1 test in 0.002s
OK
Pytest默认情况下会捕获stdout / stderr,因此它不会污染测试报告。
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更易于使用,但是您可以使它在任何一个中都能工作。
答案 1 :(得分:3)
虽然解析器在解析特定参数时可能会引发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
答案 2 :(得分:1)
如果查看错误日志,可以看到引发了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()
答案 3 :(得分:1)
如果你查看argparse的源代码,在argparse.py
,第1732行(我的python版本是3.5.1),有一个名为ArgumentParser
的{{1}}方法。代码是:
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
。
答案 4 :(得分:1)
有了上面的许多出色答案,我发现在setUp方法中,在测试内部创建了一个解析器实例,并在其中添加了一个参数,从而有效地使测试成为argparse's实施。当然,这可能是有效的测试/用例,但不一定测试argparse的脚本或应用程序的特定使用。 我认为Yauhen Yakimovich's answer对于如何以务实的方式利用argparse具有很好的见解。尽管我还没有完全接受它,但我认为可以通过解析器生成器和覆盖来简化测试方法。
我选择测试我的代码,而不是argparse's实现。为了实现这一点,我们将要利用工厂在包含所有参数定义的代码中创建解析器。这有助于在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)
这可能不是绝对最佳的答案,但是我发现使用我们自己的解析器的参数并通过简单地测试一个我们只应该在测试本身中发生的异常来测试该部分就很好了。
答案 5 :(得分:0)
我也遇到了类似的问题,具有相同的argparse错误(出口2),并对其进行了纠正,使其捕获了parse_known_args()返回的元组的第一个元素,即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'])
答案 6 :(得分:0)
我知道这是一个老问题,但是只是为了扩展@ 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())