我有一个使用argparse库的Python模块。如何为代码库的该部分编写测试?
foo()
”。sys.argv
如果是这样的话,嘲笑就是答案。看一下cli-test-helpers Python软件包。另请参阅stackoverflow.com/a/58594599/202834
我有一个使用argparse库的Python模块。如何为代码库的该部分编写测试?
foo()
”。sys.argv
如果是这样的话,嘲笑就是答案。看一下cli-test-helpers Python软件包。另请参阅stackoverflow.com/a/58594599/202834
Answers:
您应该重构代码并将解析移至函数:
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
并不是很友好的测试,因为它可以直接打印到sys.stderr
...
“ argparse部分”有点含糊不清,因此该答案仅集中在一部分:parse_args
方法上。这是与命令行交互并获取所有传递的值的方法。基本上,您可以模拟parse_args
返回的内容,因此实际上不需要从命令行获取值。该mock
软件包可以通过pip安装,适用于python 2.6-3.2版本。unittest.mock
从版本3.3开始,它是标准库的一部分。
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
即使它们没有被传递。赋予这些args值为None
。(请参阅docs)此样式对于快速进行测试(对于每个方法参数传递不同值的情况)很有用。如果您选择模拟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())
argparse
其Namespace
类。你应该嘲笑Namespace
。
from unittest import mock
现在是正确的导入方法-以及至少python3
Namespace
正是我在寻找该类的用途。尽管测试仍然依赖argparse
,但它并不依赖于argparse
被测代码的特定实现,这对我的单元测试很重要。此外,使用pytest
的parametrize()
方法可以轻松地使用包含的模板化模拟程序快速测试各种参数组合return_value=argparse.Namespace(accumulate=accumulate, integers=integers)
。
让您的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')
parse_args
抛出a 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())
将结果从argparse.ArgumentParser.parse_args
传递给函数时,有时会使用a 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()
为了测试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 == "?"
...