如何为python模块的argparse部分编写测试?[关闭]


162

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


argparse是命令行界面。编写测试以通过命令行调用应用程序。
2013年

你的问题使得它很难理解什么你想测试。我会怀疑最终是这样,例如“当我使用命令行参数X,Y,Z然后调用函数时foo()”。sys.argv如果是这样的话,嘲笑就是答案。看一下cli-test-helpers Python软件包。另请参阅stackoverflow.com/a/58594599/202834
Peterino,

Answers:


214

您应该重构代码并将解析移至函数:

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

4
感谢您的回答。当未传递特定参数时,我们如何测试错误?
Pratik Khadloya 2015年

3
@PratikKhadloya如果参数是必需的且未传递,则argparse将引发异常。
维克多·凯尔兹

2
@PratikKhadloya是的,不幸的是,该消息没有真正的帮助:(它只是2... argparse并不是很友好的测试,因为它可以直接打印到sys.stderr...
Viktor Kerkez,2015年

1
@ViktorKerkez您可能可以模拟sys.stderr来检查特定消息,无论是模拟.assert_Called_with还是通过检查模拟调用,请参阅docs.python.org/3/library/unittest.mock.html以获得更多详细信息。另请参见stackoverflow.com/questions/6271947/…,以获得模拟stdin的示例。(stderr应该类似)
BryCoBat 2015年

1
@PratikKhadloya看到了我的处理/测试错误的答案stackoverflow.com/a/55234595/1240268
Andy Hayden

25

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

但是现在您的单元测试代码也取决于argparseNamespace类。你应该嘲笑Namespace
imrek

1
@DrunkenMaster抱歉道歉。我用解释和可能的用法更新了答案。我也在这里学习,因此,如果您愿意,您(或其他人)是否可以提供模拟返回值有益的案例?(或者至少在模拟返回值的情况下是有害的)
munsu

1
from unittest import mock现在是正确的导入方法-以及至少python3
迈克尔·霍尔

1
@MichaelHall谢谢。我更新了代码段并添加了上下文信息。
munsu

1
Namespace正是我在寻找该类的用途。尽管测试仍然依赖argparse,但它并不依赖于argparse被测代码的特定实现,这对我的单元测试很重要。此外,使用pytestparametrize()方法可以轻松地使用包含的模板化模拟程序快速测试各种参数组合return_value=argparse.Namespace(accumulate=accumulate, integers=integers)
丙酮

17

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

9
  1. 使用sys.argv.append(),然后调用 parse(),填充arg列表,检查结果并重复。
  2. 从带有您的标志和转储args标志的批处理/ bash文件中调用。
  3. 将所有参数解析放在一个单独的文件中,然后在if __name__ == "__main__":调用解析中转储/评估结果,然后从批处理/ bash文件进行测试。

9

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

from unittest.mock import patch

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

如果argparse实现更改但足以进行快速测试脚本,则此操作会中断。无论如何,在测试脚本中,敏感性比特异性要重要得多。


6

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

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

有很多的测试的例子argparselib/test/test_argparse.py


5

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

2

将结果从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()

0

为了测试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 == "?"
    ...
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.