使用unittest测试argparse - 退出错误

时间:2016-08-18 22:04:32

标签: python unit-testing python-3.x argparse python-unittest

离开Greg Haskin's answer in this question时,我尝试进行单元测试,检查argparse在传递choices中不存在的一些args时是否给出了相应的错误。但是,unittest使用下面的try/except语句生成误报。

此外,当我仅使用with assertRaises语句进行测试时,argparse强制系统退出,程序不再执行任何测试。

我希望能够对此进行测试,但考虑到argparse出错时可能是多余的?

#!/usr/bin/env python3

import argparse
import unittest

class sweep_test_case(unittest.TestCase):
    """Tests that the merParse class works correctly"""

    def setUp(self):
        self.parser=argparse.ArgumentParser()
        self.parser.add_argument(
            "-c", "--color",
            type=str,
            choices=["yellow", "blue"],
            required=True)

    def test_required_unknown_TE(self):
        """Try to perform sweep on something that isn't an option.
        Should return an attribute error if it fails.
        This test incorrectly shows that the test passed, even though that must
        not be true."""
        args = ["--color", "NADA"]
        try:
            self.assertRaises(argparse.ArgumentError, self.parser.parse_args(args))
        except SystemExit:
            print("should give a false positive pass")

    def test_required_unknown(self):
        """Try to perform sweep on something that isn't an option.
        Should return an attribute error if it fails.
        This test incorrectly shows that the test passed, even though that must
        not be true."""
        args = ["--color", "NADA"]
        with self.assertRaises(argparse.ArgumentError):
            self.parser.parse_args(args)

if __name__ == '__main__':
    unittest.main()

错误:

Usage: temp.py [-h] -c {yellow,blue}
temp.py: error: argument -c/--color: invalid choice: 'NADA' (choose from 'yellow', 'blue')
E
usage: temp.py [-h] -c {yellow,blue}
temp.py: error: argument -c/--color: invalid choice: 'NADA' (choose from 'yellow', 'blue')
should give a false positive pass
.
======================================================================
ERROR: test_required_unknown (__main__.sweep_test_case)
Try to perform sweep on something that isn't an option.
----------------------------------------------------------------------
Traceback (most recent call last): #(I deleted some lines)
  File "/Users/darrin/anaconda/lib/python3.5/argparse.py", line 2310, in _check_value
    raise ArgumentError(action, msg % args)
argparse.ArgumentError: argument -c/--color: invalid choice: 'NADA' (choose from 'yellow', 'blue')

During handling of the above exception, another exception occurred:

Traceback (most recent call last): #(I deleted some lines)
  File "/anaconda/lib/python3.5/argparse.py", line 2372, in exit
    _sys.exit(status)
SystemExit: 2

7 个答案:

答案 0 :(得分:4)

这里的技巧是捕获{ "configuration": { "load": { "destinationTable": { "projectId": "<var_destinationProject>", "datasetId": "<var_destinationDataset>", "tableId": "<var_destinationTable>_<var_date>" }, "sourceUris": [ "gs://<var_bucket>/<var_name>" ], "writeDisposition": "<var_writeDeposition>", "fieldDelimiter": "|", "skipLeadingRows": 1, "allowQuotedNewLines": true, "allowJaggedRows": true, "autodetect": true } }, "jobReference": { "jobId": "<var_get_job_id_return>" } } 而不是SystemExit。这是重写您的测试以捕获ArgumentError

SystemExit

现在可以正常运行,并且测试通过:

#!/usr/bin/env python3

import argparse
import unittest

class SweepTestCase(unittest.TestCase):
    """Tests that the merParse class works correctly"""

    def setUp(self):
        self.parser=argparse.ArgumentParser()
        self.parser.add_argument(
            "-c", "--color",
            type=str,
            choices=["yellow", "blue"],
            required=True)

    def test_required_unknown(self):
        """ Try to perform sweep on something that isn't an option. """
        args = ["--color", "NADA"]
        with self.assertRaises(SystemExit):
            self.parser.parse_args(args)

if __name__ == '__main__':
    unittest.main()

但是,您可以看到正在打印用法消息,因此您的测试输出有些混乱。检查使用情况消息是否包含“无效选择”也可能很好。

您可以通过修补$ python scratch.py usage: scratch.py [-h] -c {yellow,blue} scratch.py: error: argument -c/--color: invalid choice: 'NADA' (choose from 'yellow', 'blue') . ---------------------------------------------------------------------- Ran 1 test in 0.001s OK 来做到这一点:

sys.stderr

现在您只看到常规测试报告:

#!/usr/bin/env python3

import argparse
import unittest
from io import StringIO
from unittest.mock import patch


class SweepTestCase(unittest.TestCase):
    """Tests that the merParse class works correctly"""

    def setUp(self):
        self.parser=argparse.ArgumentParser()
        self.parser.add_argument(
            "-c", "--color",
            type=str,
            choices=["yellow", "blue"],
            required=True)

    @patch('sys.stderr', new_callable=StringIO)
    def test_required_unknown(self, mock_stderr):
        """ Try to perform sweep on something that isn't an option. """
        args = ["--color", "NADA"]
        with self.assertRaises(SystemExit):
            self.parser.parse_args(args)
        self.assertRegexpMatches(mock_stderr.getvalue(), r"invalid choice")


if __name__ == '__main__':
    unittest.main()

对于pytest用户,这是不检查消息的等效项。

$ python scratch.py
.
----------------------------------------------------------------------
Ran 1 test in 0.002s

OK

Pytest默认情况下会捕获stdout / stderr,因此它不会污染测试报告。

import argparse

import pytest


def test_required_unknown():
    """ Try to perform sweep on something that isn't an option. """
    parser=argparse.ArgumentParser()
    parser.add_argument(
        "-c", "--color",
        type=str,
        choices=["yellow", "blue"],
        required=True)
    args = ["--color", "NADA"]

    with pytest.raises(SystemExit):
        parser.parse_args(args)

您还可以使用pytest检查stdout / stderr内容:

$ pytest scratch.py
================================== test session starts ===================================
platform linux -- Python 3.6.7, pytest-3.5.0, py-1.7.0, pluggy-0.6.0
rootdir: /home/don/.PyCharm2018.3/config/scratches, inifile:
collected 1 item                                                                         

scratch.py .                                                                       [100%]

================================ 1 passed in 0.01 seconds ================================

和往常一样,我发现pytest更易于使用,但是您可以使它在任何一个中都能工作。

答案 1 :(得分:3)

虽然解析器在解析特定参数时可能会引发ArgumentError,但通常会被捕获,并传递给parser.errorparse.exit。结果是打印用法以及错误消息,然后打印sys.exit(2)

所以asssertRaises不是argparse中测试此类错误的好方法。模块的单元测试文件test/test_argparse.py有一种精心设计的方法,包括对ArgumentParser进行子类化,重新定义其error方法,以及重定向输出。

parser.parse_known_args(由parse_args调用)以:

结尾
    try:
        namespace, args = self._parse_known_args(args, namespace)
        if hasattr(namespace, _UNRECOGNIZED_ARGS_ATTR):
            args.extend(getattr(namespace, _UNRECOGNIZED_ARGS_ATTR))
            delattr(namespace, _UNRECOGNIZED_ARGS_ATTR)
        return namespace, args
    except ArgumentError:
        err = _sys.exc_info()[1]
        self.error(str(err))

=================

这个测试怎么样(我从test_argparse.py借了几个想法:

import argparse
import unittest

class ErrorRaisingArgumentParser(argparse.ArgumentParser):
    def error(self, message):
        #print(message)
        raise ValueError(message)  # reraise an error

class sweep_test_case(unittest.TestCase):
    """Tests that the Parse class works correctly"""

    def setUp(self):
        self.parser=ErrorRaisingArgumentParser()
        self.parser.add_argument(
            "-c", "--color",
            type=str,
            choices=["yellow", "blue"],
            required=True)

    def test_required_unknown(self):
        """Try to perform sweep on something that isn't an option.
        Should pass"""
        args = ["--color", "NADA"]
        with self.assertRaises(ValueError) as cm:
            self.parser.parse_args(args)
        print('msg:',cm.exception)
        self.assertIn('invalid choice', str(cm.exception))

if __name__ == '__main__':
    unittest.main()

运行:

1931:~/mypy$ python3 stack39028204.py 
msg: argument -c/--color: invalid choice: 'NADA' (choose from 'yellow', 'blue')
.
----------------------------------------------------------------------
Ran 1 test in 0.002s

OK

答案 2 :(得分:1)

如果查看错误日志,可以看到引发了argparse.ArgumentError而不是AttributeError。您的代码应如下所示:

#!/usr/bin/env python3

import argparse
import unittest
from argparse import ArgumentError

class sweep_test_case(unittest.TestCase):
    """Tests that the merParse class works correctly"""

    def setUp(self):
        self.parser=argparse.ArgumentParser()
        self.parser.add_argument(
            "-c", "--color",
            type=str,
            choices=["yellow", "blue"],
            required=True)

    def test_required_unknown_TE(self):
        """Try to perform sweep on something that isn't an option.
        Should return an attribute error if it fails.
        This test incorrectly shows that the test passed, even though that must
        not be true."""
        args = ["--color", "NADA"]
        try:
            self.assertRaises(ArgumentError, self.parser.parse_args(args))
        except SystemExit:
            print("should give a false positive pass")

    def test_required_unknown(self):
        """Try to perform sweep on something that isn't an option.
        Should return an attribute error if it fails.
        This test incorrectly shows that the test passed, even though that must
        not be true."""
        args = ["--color", "NADA"]
        with self.assertRaises(ArgumentError):
            self.parser.parse_args(args)

if __name__ == '__main__':
    unittest.main()

答案 3 :(得分:1)

如果你查看argparse的源代码,在argparse.py,第1732行(我的python版本是3.5.1),有一个名为ArgumentParser的{​​{1}}方法。代码是:

parse_known_args

因此,# parse the arguments and exit if there are any errors try: namespace, args = self._parse_known_args(args, namespace) if hasattr(namespace, _UNRECOGNIZED_ARGS_ATTR): args.extend(getattr(namespace, _UNRECOGNIZED_ARGS_ATTR)) delattr(namespace, _UNRECOGNIZED_ARGS_ATTR) return namespace, args except ArgumentError: err = _sys.exc_info()[1] self.error(str(err)) 将被ArgumentError吞噬,并以错误代码退出。如果你想要测试这个,我能想到的唯一方法是嘲笑argparse

答案 4 :(得分:1)

有了上面的许多出色答案,我发现在setUp方法中,在测试内部创建了一个解析器实例,并在其中添加了一个参数,从而有效地使测试成为argparse's实施。当然,这可能是有效的测试/用例,但不一定测试argparse的脚本或应用程序的特定使用。 我认为Yauhen Yakimovich's answer对于如何以务实的方式利用argparse具有很好的见解。尽管我还没有完全接受它,但我认为可以通过解析器生成器和覆盖来简化测试方法。

我选择测试我的代码,而不是argparse's实现。为了实现这一点,我们将要利用工厂在包含所有参数定义的代码中创建解析器。这有助于在setUp中测试我们自己的解析器。

// my_class.py
import argparse

class MyClass:
    def __init__(self):
        self.parser = self._create_args_parser()

    def _create_args_parser():
        parser = argparse.ArgumentParser()
        parser.add_argument('--kind', 
                             action='store',
                             dest='kind', 
                             choices=['type1', 'type2'], 
                             help='kind can be any of: type1, type2')

        return parser

在测试中,我们可以生成解析器并对其进行测试。我们将覆盖错误方法,以确保不会陷入argparse's ArgumentError评估中。

import unittest
from my_class import MyClass

class MyClassTest(unittest.TestCase):
    def _redefine_parser_error_method(self, message):
        raise ValueError

    def setUp(self):
        parser = MyClass._create_args_parser()
        parser.error = self._redefine_parser_error_func
        self.parser = parser

    def test_override_certificate_kind_arguments(self):
        args = ['--kind', 'not-supported']
        expected_message = "argument --kind: invalid choice: 'not-supported'.*$"

        with self.assertRaisesRegex(ValueError, expected_message):
            self.parser.parse_args(args)

这可能不是绝对最佳的答案,但是我发现使用我们自己的解析器的参数并通过简单地测试一个我们只应该在测试本身中发生的异常来测试该部分就很好了。

答案 5 :(得分:0)

我也遇到了类似的问题,具有相同的argparse错误(出口2),并对其进行了纠正,使其捕获了parse_known_args()返回的元组的第一个元素,即argparse.Namespace对象。

def test_basics_options_of_parser(self):
    parser = w2ptdd.get_parser()
    # unpacking tuple
    parser_name_space,__ = parser.parse_known_args()
    args = vars(parser_name_space)
    self.assertFalse(args['unit'])
    self.assertFalse(args['functional'])

答案 6 :(得分:0)

我知道这是一个老问题,但是只是为了扩展@ don-kirkby寻找SystemExit的答案–但不必使用pytestpatching –您可以包装如果您要声明有关错误消息的内容,请使用contextlib.redirect_stderr中的测试代码:

    import contextlib
    from io import StringIO
    import unittest
    class MyTest(unittest.TestCase):
        def test_foo(self):
            ioerr = StringIO()
            with contextlib.redirect_stderr(ioerr):
                with self.assertRaises(SystemExit) as err:
                    foo('bad')
            self.assertEqual(err.exception.code, 2)
            self.assertIn("That is a 'bad' thing", ioerr.getvalue())