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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/18160078/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-19 10:00:46  来源:igfitidea点击:

How do you write tests for the argparse portion of a python module?

pythonunit-testingargparse

提问by pydanny

I have a Python module that uses the argparse library. How do I write tests for that section of the code base?

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

采纳答案by Viktor Kerkez

You should refactor your code and move the parsing to a function:

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

def parse_args(args):
    parser = argparse.ArgumentParser(...)
    parser.add_argument...
    # ...Create your parser as you like...
    return parser.parse_args(args)

Then in your mainfunction you should just call it with:

然后在您的main函数中,您应该使用以下命令调用它:

parser = parse_args(sys.argv[1:])

(where the first element of sys.argvthat represents the script name is removed to not send it as an additional switch during CLI operation.)

(其中sys.argv表示脚本名称的第一个元素被删除,以便在 CLI 操作期间不将其作为附加开关发送。)

In your tests, you can then call the parser function with whatever list of arguments you want to test it with:

在您的测试中,您可以使用要测试的任何参数列表调用解析器函数:

def test_parser(self):
    parser = parse_args(['-l', '-m'])
    self.assertTrue(parser.long)
    # ...and so on.

This way you'll never have to execute the code of your application just to test the parser.

这样您就不必为了测试解析器而执行应用程序的代码。

If you need to change and/or add options to your parser later in your application, then create a factory method:

如果您稍后需要在应用程序中更改和/或向解析器添加选项,请创建一个工厂方法:

def create_parser():
    parser = argparse.ArgumentParser(...)
    parser.add_argument...
    # ...Create your parser as you like...
    return parser

You can later manipulate it if you want, and a test could look like:

如果需要,您可以稍后对其进行操作,并且测试可能如下所示:

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

回答by Steve Barnes

  1. Populate your arg list by using sys.argv.append()and then call parse(), check the results and repeat.
  2. Call from a batch/bash file with your flags and a dump args flag.
  3. Put all your argument parsing in a separate file and in the if __name__ == "__main__":call parse and dump/evaluate the results then test this from a batch/bash file.
  1. 使用sys.argv.append()然后调用 填充您的 arg 列表parse(),检查结果并重复。
  2. 从带有您的标志和转储 args 标志的批处理/bash 文件中调用。
  3. 将所有参数解析放在一个单独的文件中,并在if __name__ == "__main__":调用中解析并转储/评估结果,然后从批处理/bash 文件中进行测试。

回答by hpaulj

A simple way of testing a parser is:

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

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

Another way is to modify sys.argv, and call args = parser.parse_args()

另一种方法是修改sys.argv,并调用args = parser.parse_args()

There are lots of examples of testing argparsein lib/test/test_argparse.py

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

回答by Ceasar Bautista

Make your main()function take argvas an argument rather than letting it read from sys.argvas it will by default:

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

Then you can test normally.

然后就可以正常测试了。

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

回答by pdxjohnny

I found that the easiest way, for me at least, was just to check sys.argv[0] so see if python was ran as python -m unittestand not parse anything if that was the case.

我发现最简单的方法,至少对我来说,就是检查 sys.argv[0] 所以看看 python 是否运行,如果是这样的话python -m unittest,不解析任何东西。

import sys
import argparse

parser = argparse.ArgumentParser()

parser.add_argument('--outdir', help='Directory to output to', \
    default='out')
parser.add_argument('--file', help='Input file', \
    default='section')
parser.add_argument('--word', help='Word to look up')

if sys.argv[0] == 'python -m unittest':
    args = parser.parse_args([])
else:
    args = parser.parse_args()

回答by munsu

"argparse portion" is a bit vague so this answer focuses on one part: the parse_argsmethod. This is the method that interacts with your command line and gets all the passed values. Basically, you can mock what parse_argsreturns so that it doesn't need to actually get values from the command line. The mockpackagecan be installed via pip for python versions 2.6-3.2. It's part of the standard library as unittest.mockfrom version 3.3 onwards.

“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

You have to include all your command method's args in Namespaceeven if they're not passed. Give those args a value of None. (see the docs) This style is useful for quickly doing testing for cases where different values are passed for each method argument. If you opt to mock Namespaceitself for total argparse non-reliance in your tests, make sure it behaves similarly to the actual Namespaceclass.

Namespace即使没有传递,您也必须包含所有命令方法的参数。给这些 args 一个值None。(请参阅文档)此样式对于快速测试为每个方法参数传递不同值的情况很有用。如果您选择在测试中模拟Namespace自己完全不依赖 argparse,请确保它的行为与实际Namespace类相似。

Below is an example using the first snippet from the argparse library.

下面是使用 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())

回答by ???

I did not want to modify the original serving script so I just mocked out the sys.argvpart in argparse.

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

from unittest.mock import patch

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

This breaks if argparse implementation changes but enough for a quick test script. Sensibility is much more important than specificity in test scripts anyways.

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

回答by guest

When passing results from argparse.ArgumentParser.parse_argsto a function, I sometimes use a namedtupleto mock arguments for testing.

将结果传递argparse.ArgumentParser.parse_args给函数时,我有时会使用 anamedtuple来模拟测试参数。

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

回答by Andy Hayden

parse_argsthrows a SystemExitand prints to stderr, you can catch both of these:

parse_argsthrows aSystemExit并打印到 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

You inspect stderr (using err.seek(0); err.read()but generally that granularity isn't required.

您检查 stderr (使用err.seek(0); err.read()但通常不需要粒度。

Now you can use assertTrueor whichever testing you like:

现在您可以使用assertTrue或任何您喜欢的测试:

assertTrue(validate_args(["-l", "-m"]))


Alternatively you might like to catch and rethrow a different error (instead of SystemExit):

或者,您可能希望捕获并重新抛出不同的错误(而不是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())

回答by vczm

For testing CLI (command line interface), and not command outputyou need to do a something like this

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