如何为python模块的argparse部分编写测试?

我有一个使用argparse库的Python模块。如何为这部分代码库编写测试?

103390 次浏览
  1. 通过使用sys.argv.append()填充参数列表,然后调用 parse(),检查结果并重复
  2. 使用标记和dump args标记从批处理/bash文件调用。
  3. 将所有参数解析放在一个单独的文件中,并在if __name__ == "__main__":调用中解析和转储/计算结果,然后从批处理/bash文件中测试。

你应该重构你的代码,并将解析移动到一个函数:

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

测试解析器的一个简单方法是:

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

lib/test/test_argparse.py中有很多测试argparse的例子

让你的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')

"argparse part "有点模糊,所以这个答案集中在一个部分:parse_args方法。这是与命令行交互并获取所有传递值的方法。基本上,你可以模拟parse_args返回的内容,这样它就不需要实际从命令行获取值。对于python版本2.6-3.2,可以通过pip安装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())

我不想修改原来的服务脚本,所以我只是在argparse中模拟出sys.argv部分。

from unittest.mock import patch


with patch('argparse._sys.argv', ['python', 'serve.py']):
...  # your test code here

如果argparse实现发生变化,这就会中断,但对于快速测试脚本来说已经足够了。在测试脚本中,敏感性要比特异性重要得多。

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

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

为了测试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 == "?"
...