如何为 python 模块的 argparse 部分编写测试?
- 2025-03-18 08:55:00
- admin 原创
- 43
问题描述:
我有一个使用 argparse 库的 Python 模块。我该如何为该部分代码库编写测试?
解决方案 1:
您应该重构代码并将解析移至函数:
def parse_args(args):
parser = argparse.ArgumentParser(...)
parser.add_argument...
# ...Create your parser as you like...
return parser.parse_args(args)
然后在您的main
函数中您只需使用以下命令调用它:
parser = parse_args(sys.argv[1:])
(其中代表脚本名称的第一个元素sys.argv
被删除,以便在 CLI 操作期间不将其作为附加开关发送。)
然后,在测试中,您可以使用要测试的任何参数列表来调用解析器函数:
def test_parser(self):
parser = parse_args(['-l', '-m'])
self.assertTrue(parser.long)
# ...and so on.
这样,您就不需要执行应用程序的代码来测试解析器了。
如果您稍后需要在应用程序中更改和/或向解析器添加选项,请创建一个工厂方法:
def create_parser():
parser = argparse.ArgumentParser(...)
parser.add_argument...
# ...Create your parser as you like...
return parser
如果您愿意,可以稍后对其进行操作,测试可能如下所示:
class ParserTest(unittest.TestCase):
def setUp(self):
self.parser = create_parser()
def test_something(self):
parsed = self.parser.parse_args(['--something', 'test'])
self.assertEqual(parsed.something, 'test')
解决方案 2:
“argparse 部分”有点模糊,所以这个答案主要关注一个部分:parse_args
方法。这是与命令行交互并获取所有传递的值的方法。基本上,您可以模拟parse_args
返回的内容,这样它就不需要真正从命令行获取值。可以通过 pip 安装适用于 python 版本 2.6-3.2 的mock
包。从 3.3 版开始,它是标准库的一部分unittest.mock
。
import argparse
try:
from unittest import mock # python 3.3+
except ImportError:
import mock # python 2.6-3.2
@mock.patch('argparse.ArgumentParser.parse_args',
return_value=argparse.Namespace(kwarg1=value, kwarg2=value))
def test_command(mock_args):
pass
Namespace
即使没有传递,您也必须包含所有命令方法的参数。为这些参数赋予一个值None
。(请参阅文档)此样式对于快速测试为每个方法参数传递不同值的情况非常有用。如果您选择Namespace
在测试中模拟自身以完全不依赖 argparse,请确保它的行为与实际Namespace
类类似。
下面是使用 argparse 库中第一个代码片段的示例。
# test_mock_argparse.py
import argparse
try:
from unittest import mock # python 3.3+
except ImportError:
import mock # python 2.6-3.2
def main():
parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument('integers', metavar='N', type=int, nargs='+',
help='an integer for the accumulator')
parser.add_argument('--sum', dest='accumulate', action='store_const',
const=sum, default=max,
help='sum the integers (default: find the max)')
args = parser.parse_args()
print(args) # NOTE: this is how you would check what the kwargs are if you're unsure
return args.accumulate(args.integers)
@mock.patch('argparse.ArgumentParser.parse_args',
return_value=argparse.Namespace(accumulate=sum, integers=[1,2,3]))
def test_command(mock_args):
res = main()
assert res == 6, "1 + 2 + 3 = 6"
if __name__ == "__main__":
print(main())
解决方案 3:
让你的main()
函数argv
作为参数,而不是让它像默认的那样读取sys.argv
:
# mymodule.py
import argparse
import sys
def main(args):
parser = argparse.ArgumentParser()
parser.add_argument('-a')
process(**vars(parser.parse_args(args)))
return 0
def process(a=None):
pass
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
然后就可以正常测试了。
import mock
from mymodule import main
@mock.patch('mymodule.process')
def test_main(process):
main([])
process.assert_call_once_with(a=None)
@mock.patch('foo.process')
def test_main_a(process):
main(['-a', '1'])
process.assert_call_once_with(a='1')
解决方案 4:
我不想修改原始的服务脚本所以我只是模拟了sys.argv
argparse 中的部分。
from unittest.mock import patch
with patch('argparse._sys.argv', ['python', 'serve.py']):
... # your test code here
如果 argparse 实现发生变化,这将中断,但对于快速测试脚本来说已经足够了。无论如何,在测试脚本中,敏感性比特异性更重要。
解决方案 5:
parse_args
抛出SystemExit
并打印到 stderr,您可以捕获以下两者:
import contextlib
import io
import sys
@contextlib.contextmanager
def captured_output():
new_out, new_err = io.StringIO(), io.StringIO()
old_out, old_err = sys.stdout, sys.stderr
try:
sys.stdout, sys.stderr = new_out, new_err
yield sys.stdout, sys.stderr
finally:
sys.stdout, sys.stderr = old_out, old_err
def validate_args(args):
with captured_output() as (out, err):
try:
parser.parse_args(args)
return True
except SystemExit as e:
return False
您检查 stderr(使用err.seek(0); err.read()
但通常不需要该粒度。
现在您可以使用assertTrue
或任何您喜欢的测试:
assertTrue(validate_args(["-l", "-m"]))
或者,您可能希望捕获并重新抛出不同的错误(而不是SystemExit
):
def validate_args(args):
with captured_output() as (out, err):
try:
return parser.parse_args(args)
except SystemExit as e:
err.seek(0)
raise argparse.ArgumentError(err.read())
解决方案 6:
使用 填充您的参数列表
sys.argv.append()
,然后调用parse()
,检查结果并重复。使用您的标志和 dump args 标志从批处理/bash 文件中调用。
将所有参数解析放在单独的文件中,然后在
if __name__ == "__main__":
调用中解析并转储/评估结果,然后从批处理/bash 文件中进行测试。
解决方案 7:
测试解析器的一个简单方法是:
parser = ...
parser.add_argument('-a',type=int)
...
argv = '-a 1 foo'.split() # or ['-a','1','foo']
args = parser.parse_args(argv)
assert(args.a == 1)
...
另一种方法是修改sys.argv
,然后调用args = parser.parse_args()
有很多测试的argparse
例子lib/test/test_argparse.py
解决方案 8:
当将结果传递argparse.ArgumentParser.parse_args
给函数时,我有时会使用namedtuple
模拟参数进行测试。
import unittest
from collections import namedtuple
from my_module import main
class TestMyModule(TestCase):
args_tuple = namedtuple('args', 'arg1 arg2 arg3 arg4')
def test_arg1(self):
args = TestMyModule.args_tuple("age > 85", None, None, None)
res = main(args)
assert res == ["55289-0524", "00591-3496"], 'arg1 failed'
def test_arg2(self):
args = TestMyModule.args_tuple(None, [42, 69], None, None)
res = main(args)
assert res == [], 'arg2 failed'
if __name__ == '__main__':
unittest.main()
解决方案 9:
为了测试 CLI(命令行界面),而不是命令输出,我做了类似的事情
import pytest
from argparse import ArgumentParser, _StoreAction
ap = ArgumentParser(prog="cli")
ap.add_argument("cmd", choices=("spam", "ham"))
ap.add_argument("-a", "--arg", type=str, nargs="?", default=None, const=None)
...
def test_parser():
assert isinstance(ap, ArgumentParser)
assert isinstance(ap, list)
args = {_.dest: _ for _ in ap._actions if isinstance(_, _StoreAction)}
assert args.keys() == {"cmd", "arg"}
assert args["cmd"] == ("spam", "ham")
assert args["arg"].type == str
assert args["arg"].nargs == "?"
...
解决方案 10:
来自我的博客文章的最小完整示例(https://hughesadam87.medium.com/dead-simple-pytest-and-argparse-d1dbb6affbc3)
# coolapp.py
import argparse as ap
import sys
def _parse(args) -> ap.Namespace:
parser = ap.ArgumentParser()
parser.add_argument("myfile")
parsed = parser.parse_args(args)
start(parsed.myfile
def start(myfile) -> None:
print(f'my file: {myfile}')
if __name__ == "__main__":
_parse(sys.argv[1:])
一些测试
#test_coolapp.py
from coolapp import start, _parse
import sys
def test_coolapp():
""" Direct import of start """
start("myfile.txt")
def test_coolapp_sysargs():
""" Called through __main__ (eg. python coolapp.py myfile.txt) """
_parse(['myfile.txt'])
def test_coolapp_no_args(capsys):
""" ie. python coolapp.py """
with pytest.raises(SystemExit):
_parse([])
captured = capsys.readouterr()
assert "the following arguments are required: myfile" in captured.err
def test_coolapp_extra_args(capsys):
""" ie. python coolapp.py arg1 arg2 """
with pytest.raises(SystemExit):
_parse(['arg1', 'arg2'])
captured = capsys.readouterr()
assert "unrecognized arguments: arg2" in captured.err
解决方案 11:
除了许多好的答案之外...
就我而言,我应该向解析参数的函数提供参数,例如:
# main.py
import argparse
def get_myparam():
parser = argparse.ArgumentParser()
parser.add_argument('--myparam', type=str, default='myvalue')
args = parser.parse_args()
return args.myparam
if __name__ == '__main__':
print(f'main: myparam={get_myparam()}')
输出示例:
$ python main.py
main: myparam=myvalue
$ python main.py --myparam newvalue
main: myparam=newvalue
pytest
测试示例:
# test_main.py
import argparse
import main
def test_mock_params(mocker):
mocker.patch('argparse.ArgumentParser.parse_args',
return_value=argparse.Namespace(myparam='mocked',))
assert main.get_myparam() == 'mocked'
您mocker
需要安装pytest-mock
:
$ pip install pytest-mock
扫码咨询,免费领取项目管理大礼包!