【问题标题】:pytest and argparse: code works in use, but tests failingpytest 和 argparse:代码在使用中,但测试失败
【发布时间】:2020-05-02 03:57:36
【问题描述】:

我想在我正在编写的一些代码中添加一些命令行参数,但当我包含 argparse 内容时测试失败。

这是基类的精简版:

import argparse
import sys


class PreProcessor:

    def parse_args(self, args):
        parser = argparse.ArgumentParser(description='Arguments for PreProcessor scripts.')
        parser.add_argument('-i', '--ignore-pid', help='If the script is already running, it will not re-run. This over-rides that.', action="store_true")
        parser.add_argument('-v', '--verbose', type=int, choices=[0,1,2,3], default=1, help='Be noisy when running [0 is completely silent, 3 is debug-level. defaults to 1].')
        return parser.parse_args()

    def __init__(
        self,
        code,
    ):
        if not code:
            raise ValueError("A code must be defined")
        self.code = code

        # These two lines
        self.args = self.parse_args(sys.argv)
        print(f"type: {type(self.args)}, data: {self.args}")

....这是它的测试文件:

import pytest
from .example import PreProcessor


def test_base_initialisation():
    foo = PreProcessor(code="foo")
    assert foo.code == "foo"

我得到的错误是:

platform linux -- Python 3.6.9, pytest-3.3.2, py-1.5.2, pluggy-0.6.0 -- /usr/bin/python3
cachedir: .cache
rootdir: /home/kiz/development/FindingStudySpaces/preprocessors, inifile: pytest.ini
plugins: cov-2.5.1
collected 1 item                                                                                                                                                                                                                                                       
preprocessors/test_example.py::test_base_initialisation FAILED                     [100%]

 generated xml file: /home/kiz/development/FindingStudySpaces/preprocessors/pytest-results.xml 

----------- coverage: platform linux, python 3.6.9-final-0 -----------
Name                             Stmts   Miss  Cover
----------------------------------------------------
preprocessors/__init__.py            0      0   100%
preprocessors/example.py            14      2    86%
preprocessors/preprocessors.py     140    140     0%
----------------------------------------------------
TOTAL                              154    142     8%
Coverage HTML written to dir htmlcov

======================================== FAILURES ========================================
________________________________ test_base_initialisation ________________________________

self = ArgumentParser(prog='pytest-3', usage=None, description='Arguments for PreProcessor scripts.', formatter_class=<class 'argparse.HelpFormatter'>, conflict_handler='error', add_help=True)
action = _StoreAction(option_strings=['-v', '--verbose'], dest='verbose', nargs=None, const=None, default=1, type=<class 'int'>...es=[0, 1, 2, 3], help='Be noisy when running [0 is completely silent, 3 is debug-level. defaults to 1].', metavar=None)
arg_string = 'preprocessors/test_example.py'

    def _get_value(self, action, arg_string):
        type_func = self._registry_get('type', action.type, action.type)
        if not callable(type_func):
            msg = _('%r is not callable')
            raise ArgumentError(action, msg % type_func)
    
        # convert the value to the appropriate type
        try:
>           result = type_func(arg_string)
E           ValueError: invalid literal for int() with base 10: 'preprocessors/test_example.py'
         ... <snip loads of stuff> ...
                 # TypeErrors or ValueErrors also indicate errors
        except (TypeError, ValueError):
            name = getattr(action.type, '__name__', repr(action.type))
            args = {'type': name, 'value': arg_string}
            msg = _('invalid %(type)s value: %(value)r')
>           raise ArgumentError(action, msg % args)
E           argparse.ArgumentError: argument -v/--verbose: invalid int value: 'preprocessors/test_example.py'
         ... <snip loads more stuff> ...

我尝试在 init 方法中传入 sys.argv[1:] - 没有区别。

如果我注释掉对argparse 的调用(即它显示# These two lines 的行),那么我就可以通过了..

我真的不想为每个测试方法添加模拟/补丁,也不想在实时代码中添加一些子句来测试def parse_args(self, args)是否已被测试例程调用

....我的 google-foo 发现了一些关于测试参数传递的讨论(这很好) - 但我找不到关于 argparse 在这个级别失败的任何信息。

【问题讨论】:

  • pytestargparse 很棘手。 argparse 通常会解析 sys.argv。但是测试框架也使用sys.argv 作为输入。你不能同时和两个人说话。
  • @hpaulj - 谢谢你......它把我推向了正确的方向

标签: pytest argparse


【解决方案1】:

感谢两位...这对我来说确实有效...

sys.argv[0] 中查找__init__ 以查找pytest(它真的应该查找unittestnode),如果确实如此,则将一个空列表传递给argparser 函数:

import argparse
import re
import sys

class PreProcessor:

def parse_args(self, args):
    parser = argparse.ArgumentParser(description='Arguments for PreProcessor scripts.')
    parser.add_argument('-i', '--ignore-pid', help='If the script is already running, it will not re-run. This over-rides that.', action="store_true")
    parser.add_argument('-v', '--verbose', type=int, choices=[0,1,2,3], default=1, help='Be noisy when running [0 is completely silent, 3 is debug-level. defaults to 1].')
    return parser.parse_args(args)
   
def __init__(
    self,
    code,
):
    if not code:
        raise ValueError("A code must be defined")
    self.code = code

    if re.search('pytest', sys.argv[0]):
        self.args = self.parse_args([])
    else:
        self.args = self.parse_args(sys.argv[1:])

现在,在我的测试中,我可以根据需要patch sys 对象:

import pytest
import re
import sys

from unittest.mock import patch

from .example import PreProcessor


def test_base_initialisation():
    foo = PreProcessor(code="foo")
    assert foo.code == "foo"

def test_known_command_line_options(capsys):
    foo = PreProcessor(code="foo")
    test_args = ["fake", "-h"]
    with patch.object(sys, 'argv', test_args):
        with pytest.raises(SystemExit):
            foo.parse_args(sys.argv)
            captured = capsys.readouterr()
            assert re.match(captured, "usage: fake [-h] [-i] [-v {0,1,2,3}]")
            assert re.search(captured, "Arguments for PreProcessor scripts")

    test_args = ["fake", "--help"]
    with patch.object(sys, 'argv', test_args):
        with pytest.raises(SystemExit):
            foo.parse_args(sys.argv)
            captured = capsys.readouterr()
            assert re.match(captured, "usage: fake [-h] [-i] [-v {0,1,2,3}]")
            assert re.search(captured, "Arguments for PreProcessor scripts")

def test_unknown_command_line_options(capsys):
    foo = PreProcessor(code="foo")
    test_args = ["fake", "-a"]
    with patch.object(sys, 'argv', test_args):
        with pytest.raises(SystemExit):
            foo.parse_args(sys.argv)
            captured = capsys.readouterr()
            assert re.match(captured, "usage: fake [-h] [-i] [-v {0,1,2,3}]")
            assert re.search(captured, "unrecognized arguments: -a")

作为参考,来自 argparse 的帮助文本如下:

$> python3 labmon.py -h 
usage: labmon.py [-h] [-i] [-v {0,1,2,3}]

Arguments for PreProcessor scripts.

optional arguments:
  -h, --help            show this help message and exit
  -i, --ignore-pid      If the script is already running, it will not re-run.
                        This over-rides that.
  -v {0,1,2,3}, --verbose {0,1,2,3}
                        Be noisy when running [0 is completely silent, 3 is
                        debug-level. defaults to 1].

$> python3 labmon.py -a  
usage: labmon.py [-h] [-i] [-v {0,1,2,3}]
labmon.py: error: unrecognized arguments: -a

【讨论】:

    【解决方案2】:

    这一行:

            return parser.parse_args()
    

    应该是

            return parser.parse_args(args)
    

    否则,parser.parse_args 默认为 sys.argv[1:],这可能不是您想要的

    还有 sys.argv 传递给它的是您的 pytest 调用的值——您可能希望将其设置为对您的程序有意义的东西

    你有~有点两种选择

    1. monkeypatch sys.argv:
    from unittest import mock
    
    def test():
        with mock.patch.object(sys, 'argv', ['yourprog', 'args', 'here']):
            ...
    
    1. 依赖注入您的参数并通过您的程序传递它们:
    class C:
        def __init__(self, code, args=None):
            ...
            self.parse_args(args)
    
        def parse_args(self, args=None):
            ...
            return parser.parse_args(args)
    
      +
    def test():
        thing = C(..., args=['args', 'here'])
    

    【讨论】:

      猜你喜欢
      • 2016-11-02
      • 1970-01-01
      • 2019-12-17
      • 2020-03-12
      • 2016-06-02
      • 2013-08-21
      • 2015-11-30
      • 1970-01-01
      • 2017-12-25
      相关资源
      最近更新 更多