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

2025-03-18 08:55:00
admin
原创
43
摘要:问题描述:我有一个使用 argparse 库的 Python 模块。我该如何为该部分代码库编写测试?解决方案 1:您应该重构代码并将解析移至函数:def parse_args(args): parser = argparse.ArgumentParser(...) parser.add_arg...

问题描述:

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


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

解决方案 2:

“argparse 部分”有点模糊,所以这个答案主要关注一个部分:parse_args方法。这是与命令行交互并获取所有传递的值的方法。基本上,您可以模拟parse_args返回的内容,这样它就不需要真正从命令行获取值。可以通过 pip 安装适用于 python 版本 2.6-3.2 的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())

解决方案 3:

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

解决方案 4:

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

from unittest.mock import patch

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

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

解决方案 5:

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

解决方案 6:

  1. 使用 填充您的参数列表sys.argv.append(),然后调用
    parse(),检查结果并重复。

  2. 使用您的标志和 dump args 标志从批处理/bash 文件中调用。

  3. 将所有参数解析放在单独的文件中,然后在if __name__ == "__main__":调用中解析并转储/评估结果,然后从批处理/bash 文件中进行测试。

解决方案 7:

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

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

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

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

解决方案 10:

来自我的博客文章的最小完整示例(https://hughesadam87.medium.com/dead-simple-pytest-and-argparse-d1dbb6affbc3

# coolapp.py
import argparse as ap
import sys

def _parse(args) -> ap.Namespace:
    parser = ap.ArgumentParser() 
    parser.add_argument("myfile")
    parsed = parser.parse_args(args)
    start(parsed.myfile


def start(myfile) -> None:
    print(f'my file: {myfile}')


if __name__ == "__main__":
    _parse(sys.argv[1:])

一些测试

#test_coolapp.py
from coolapp import start, _parse
import sys

def test_coolapp():
    """ Direct import of start """
    start("myfile.txt")

def test_coolapp_sysargs():
    """ Called through __main__ (eg. python coolapp.py myfile.txt) """
    _parse(['myfile.txt'])

def test_coolapp_no_args(capsys):
    """ ie. python coolapp.py """
    with pytest.raises(SystemExit):
        _parse([])
    captured = capsys.readouterr()
    assert "the following arguments are required: myfile" in captured.err

def test_coolapp_extra_args(capsys):
    """ ie. python coolapp.py arg1 arg2 """
    with pytest.raises(SystemExit):
        _parse(['arg1', 'arg2'])
    captured = capsys.readouterr()
    assert "unrecognized arguments: arg2" in captured.err

解决方案 11:

除了许多好的答案之外...

就我而言,我应该向解析参数的函数提供参数,例如:

# main.py
import argparse

def get_myparam():
    parser = argparse.ArgumentParser()
    parser.add_argument('--myparam', type=str, default='myvalue')
    args = parser.parse_args()
    return args.myparam

if __name__ == '__main__':
    print(f'main: myparam={get_myparam()}')

输出示例:

$ python main.py
main: myparam=myvalue

$ python main.py --myparam newvalue
main: myparam=newvalue

pytest测试示例:

# test_main.py
import argparse

import main


def test_mock_params(mocker):
    mocker.patch('argparse.ArgumentParser.parse_args',
                 return_value=argparse.Namespace(myparam='mocked',))
    assert main.get_myparam() == 'mocked'

mocker需要安装pytest-mock

$ pip install pytest-mock
相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   2482  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1533  
  PLM(产品生命周期管理)项目对于企业优化产品研发流程、提升产品质量以及增强市场竞争力具有至关重要的意义。然而,在项目推进过程中,范围蔓延是一个常见且棘手的问题,它可能导致项目进度延迟、成本超支以及质量下降等一系列不良后果。因此,有效避免PLM项目范围蔓延成为项目成功的关键因素之一。以下将详细阐述三大管控策略,助力企业...
plm系统   0  
  PLM(产品生命周期管理)项目管理在企业产品研发与管理过程中扮演着至关重要的角色。随着市场竞争的加剧和产品复杂度的提升,PLM项目面临着诸多风险。准确量化风险优先级并采取有效措施应对,是确保项目成功的关键。五维评估矩阵作为一种有效的风险评估工具,能帮助项目管理者全面、系统地评估风险,为决策提供有力支持。五维评估矩阵概述...
免费plm软件   0  
  引言PLM(产品生命周期管理)开发流程对于企业产品的全生命周期管控至关重要。它涵盖了从产品概念设计到退役的各个阶段,直接影响着产品质量、开发周期以及企业的市场竞争力。在当今快速发展的科技环境下,客户对产品质量的要求日益提高,市场竞争也愈发激烈,这就使得优化PLM开发流程成为企业的必然选择。缺陷管理工具和六西格玛方法作为...
plm产品全生命周期管理   0  
热门文章
项目管理软件有哪些?
曾咪二维码

扫码咨询,免费领取项目管理大礼包!

云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用