【问题标题】:How do you write tests for the argparse portion of a python module?你如何为 python 模块的 argparse 部分编写测试?
【发布时间】:2013-08-12 04:13:26
【问题描述】:

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

【问题讨论】:

  • argparse 是一个命令行界面。编写测试以通过命令行调用应用程序。
  • 您的问题让您难以理解您要测试的什么。我怀疑它最终是,例如“当我使用命令行参数 X、Y、Z 时,函数 foo() 被调用”。如果是这样的话,嘲笑sys.argv 就是答案。看看cli-test-helpers Python 包。另见stackoverflow.com/a/58594599/202834
  • 投票重新开放,因为即使没有提供特定代码,这里也很清楚 OP 的要求。足够多的人同意这仍然是一个非常高票数的问题/答案对。

标签: python unit-testing argparse


【解决方案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')

【讨论】:

  • 感谢您的回答。当某个参数没有通过时,我们如何测试错误?
  • @PratikKhadloya 如果参数是必需的但未通过,argparse 将引发异常。
  • @PratikKhadloya 是的,不幸的是,该消息并没有真正的帮助:( 它只是2...argparse 不是很适合测试,因为它直接打印到sys.stderr...跨度>
  • 我认为没有必要使用参数args=sys.argv[1:] 调用ArgumentParser.parse_args-方法调用。它已经调用了ArgumentParser.parse_known_args-方法。使用参数args==None,它将使用args = _sys.argv[1:] 获取它们,其中_syssys 的别名。 (这可能是自发布答案以来的更新。)
  • 回应上面的@thomas-fauskanger:parse_args(args) 允许你从测试中传递参数——这就是这里的意图。 parse_args() 本身可以在没有 sys.argv[1:] main() 传递的情况下工作。顺便说一句,这非常有用。
【解决方案2】:

“argparse 部分”有点模糊,所以这个答案集中在一个部分:parse_args 方法。这是与您的命令行交互并获取所有传递值的方法。基本上,您可以模拟 parse_args 返回的内容,这样它就不需要从命令行实际获取值。 mock package 可以通过 pip 安装用于 python 版本 2.6-3.2。从 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。 (参见docs)这种风格对于快速测试为每个方法参数传递不同值的情况很有用。如果您选择模拟 Namespace 本身以在您的测试中完全不依赖 argparse,请确保它的行为类似于实际的 Namespace 类。

以下是使用 argparse 库中的第一个 sn-p 的示例。

# 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
  • @DrunkenMaster 为尖刻的语气道歉。我用解释和可能的用途更新了我的答案。我也在这里学习,所以如果您愿意,您(或其他人)能否提供模拟返回值有益的案例? (或者至少在 not 模拟返回值是有害的情况下)
  • from unittest import mock 现在是正确的导入方法 - 至少对于 python3 而言
  • @MichaelHall 谢谢。我更新了 sn-p 并添加了上下文信息。
  • Namespace 类的使用正是我想要的。尽管测试仍然依赖argparse,但它并不依赖被测代码对argparse 的特定实现,这对我的单元测试很重要。此外,使用pytestparametrize() 方法很容易使用包含return_value=argparse.Namespace(accumulate=accumulate, integers=integers) 的模板模拟来快速测试各种参数组合。
【解决方案3】:

让您的main() 函数将argv 作为参数,而不是让它read from sys.argv as it will by default

# 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】:

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

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

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

    【讨论】:

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

      【讨论】:

        【解决方案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()

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

        【讨论】:

        • 这应该是公认的答案:测试赋予每个参数的不同值的最简单方法。
        【解决方案7】:

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

        【讨论】:

          【解决方案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 == "?"
                ...
            

            【讨论】:

              猜你喜欢
              • 2020-02-17
              • 2013-11-04
              • 1970-01-01
              • 1970-01-01
              • 2019-01-18
              • 2010-11-17
              • 2023-03-18
              • 1970-01-01
              • 2011-04-04
              相关资源
              最近更新 更多