【问题标题】:Passing arguments (for argparse) with unittest discover使用 unittest discover 传递参数(用于 argparse)
【发布时间】:2016-05-18 03:31:51
【问题描述】:

foo 是一个深度目录嵌套的 Python 项目,在各个子目录中包含 ~30 个 unittest 文件。在foosetup.py 内,我已经在added a custom "test" command 内部运行

 python -m unittest discover foo '*test.py'

注意这里使用unittest's discovery 模式。


由于一些测试非常慢,我最近决定测试应该有“级别”。 this question 的答案很好地解释了如何让 unittestargparse 相互配合。所以现在,我可以运行 individual 单元测试文件,比如foo/bar/_bar_test.py,使用

python foo/bar/_bar_test.py --level=3

并且只运行 3 级测试。

问题是我不知道如何传递自定义标志(在这种情况下“--level = 3”使用发现。我尝试的一切都失败了,例如:

$ python -m unittest discover --level=3 foo '*test.py'
Usage: python -m unittest discover [options]

python -m unittest discover: error: no such option: --level

$ python -m --level=3 unittest discover foo '*test.py'
/usr/bin/python: No module named --level=3

如何将--level=3 传递给各个单元测试?如果可能的话,我想避免将不同级别的测试划分到不同的文件中。

赏金编辑

赏金前(精细)解决方案建议使用系统环境变量。这还不错,但我正在寻找更清洁的东西。

将多文件测试运行程序(即 python -m unittest discover foo '*test.py')更改为其他内容即可,只要:

  1. 它允许为多文件单元测试生成单个报告。
  2. 它可以以某种方式支持多个测试级别(使用问题中的技术,或使用其他一些不同的机制)。

【问题讨论】:

    标签: python command-line argparse python-unittest


    【解决方案1】:

    使用发现时无法传递参数。 来自发现的DiscoveringTestLoader 类,删除所有不匹配的文件(使用'*test.py --level=3' 消除)并仅将文件名传递给unittest.TextTestRunner

    目前可能唯一的选择是使用环境变量

    LEVEL=3 python -m unittest discoverfoo '*test.py'
    

    【讨论】:

    • 环境变量是一个有趣的想法。谢谢。我仍然希望有一些不涉及他们的东西。
    【解决方案2】:

    您遇到的问题是 unittest 参数解析器根本不理解这种语法。因此,您必须在调用 unittest 之前删除参数。

    一个简单的方法是创建一个包装器模块(比如 my_unittest.py),它会查找您的额外参数,从 sys.argv 中删除它们,然后调用 unittest 中的主条目。

    现在好一点...该包装器的代码与您已经用于单个文件案例的代码基本相同!你只需要把它放到一个单独的文件中。

    编辑:根据要求在下面添加示例代码...

    首先,运行 UT 的新文件 (my_unittest.py):

    import sys
    import unittest
    from parser import wrapper
    
    if __name__ == '__main__':
        wrapper.parse_args()
        unittest.main(module=None, argv=sys.argv)
    

    现在 parser.py 必须在一个单独的文件中以避免在 __main__ 模块中才能使全局引用起作用:

    import sys
    import argparse
    import unittest
    
    class UnitTestParser(object):
    
        def __init__(self):
            self.args = None
    
        def parse_args(self):
            # Parse optional extra arguments
            parser = argparse.ArgumentParser()
            parser.add_argument('--level', type=int, default=0)
            ns, args = parser.parse_known_args()
            self.args = vars(ns)
    
            # Now set the sys.argv to the unittest_args (leaving sys.argv[0] alone)
            sys.argv[1:] = args
    
    wrapper = UnitTestParser()
    

    最后是一个示例测试用例(project_test.py)来测试参数是否被正确解析:

    import unittest
    from parser import wrapper
    
    class TestMyProject(unittest.TestCase):
    
        def test_len(self):
            self.assertEqual(len(wrapper.args), 1)
    
        def test_level3(self):
            self.assertEqual(wrapper.args['level'], 3)
    

    现在证明:

    $ python -m my_unittest discover --level 3 . '*test.py'
    ..
    ----------------------------------------------------------------------
    Ran 2 tests in 0.000s
    
    OK
    

    【讨论】:

    • 好的,这很好。在某种程度上,实际上,我会将其翻译为基于unittest 构建的“编写你自己的单元测试包”(我不想依赖一些本地my_unittest.py)。不过,一个好主意。谢谢!
    • @AmiTavory 它不必是单独的包。你可以把它和你的单元测试放在一起,然后简单地在同一个测试包/目录/无论你做什么来交付你的 UT 中交付那个 python 文件。
    • 如果你充实了my_unittest的内容,我很乐意接受你的回答(并奖励你)。
    • 完成... 原来有一条皱纹。我需要创建一个单独的模块来创建全局对象来跟踪额外的参数。如果我直接在 my_unittest.py 中进行解析,Python 会将对象扔掉并为 unittest 类创建另一个对象。
    【解决方案3】:

    这不会使用 unittest discover 传递参数,但它会完成您正在尝试做的事情。

    这是leveltest.py。将它放在模块搜索路径中的某个位置(可能是当前目录或站点包):

    import argparse
    import sys
    import unittest
    
    # this part copied from unittest.__main__.py
    if sys.argv[0].endswith("__main__.py"):
        import os.path
        # We change sys.argv[0] to make help message more useful
        # use executable without path, unquoted
        # (it's just a hint anyway)
        # (if you have spaces in your executable you get what you deserve!)
        executable = os.path.basename(sys.executable)
        sys.argv[0] = executable + " -m leveltest"
        del os
    
    def _id(obj):
        return obj
    
    # decorator that assigns test levels to test cases (classes and methods)
    def level(testlevel):
        if unittest.level < testlevel:
            return unittest.skip("test level too low.")
        return _id
    
    def parse_args():
        parser = argparse.ArgumentParser()
        parser.add_argument('--level', type=int, default=3)
        ns, args = parser.parse_known_args(namespace=unittest)
        return ns, sys.argv[:1] + args
    
    if __name__ == "__main__":
        ns, remaining_args = parse_args()
    
        # this invokes unittest when leveltest invoked with -m flag like:
        #    python -m leveltest --level=2 discover --verbose
        unittest.main(module=None, argv=remaining_args)
    

    这是在示例 testproject.py 文件中使用它的方式:

    import unittest
    import leveltest
    
    # This is needed before any uses of the @leveltest.level() decorator
    #   to parse the "--level" command argument and set the test level when 
    #   this test file is run directly with -m
    if __name__ == "__main__":
        ns, remaining_args = leveltest.parse_args()
    
    @leveltest.level(2)
    class TestStringMethods(unittest.TestCase):
    
        @leveltest.level(5)
        def test_upper(self):
            self.assertEqual('foo'.upper(), 'FOO')
    
        @leveltest.level(3)
        def test_isupper(self):
            self.assertTrue('FOO'.isupper())
            self.assertFalse('Foo'.isupper())
    
        @leveltest.level(4)
        def test_split(self):
            s = 'hello world'
            self.assertEqual(s.split(), ['hello', 'world'])
            # check that s.split fails when the separator is not a string
            with self.assertRaises(TypeError):
                s.split(2)
    
    if __name__ == '__main__':
        # this invokes unittest when this file is executed with -m
        unittest.main(argv=remaining_args)
    

    然后您可以通过直接运行 testproject.py 来运行测试,例如:

    ~roottwo\projects> python testproject.py --level 2 -v
    test_isupper (__main__.TestStringMethods) ... skipped 'test level too low.'
    test_split (__main__.TestStringMethods) ... skipped 'test level too low.'
    test_upper (__main__.TestStringMethods) ... skipped 'test level too low.'
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.000s
    
    OK (skipped=3)
    
    ~roottwo\projects> python testproject.py --level 3 -v
    test_isupper (__main__.TestStringMethods) ... ok
    test_split (__main__.TestStringMethods) ... skipped 'test level too low.'
    test_upper (__main__.TestStringMethods) ... skipped 'test level too low.'
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.001s
    
    OK (skipped=2)
    
    ~roottwo\projects> python testproject.py --level 4 -v
    test_isupper (__main__.TestStringMethods) ... ok
    test_split (__main__.TestStringMethods) ... ok
    test_upper (__main__.TestStringMethods) ... skipped 'test level too low.'
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.001s
    
    OK (skipped=1)
    
    ~roottwo\projects> python testproject.py --level 5 -v
    test_isupper (__main__.TestStringMethods) ... ok
    test_split (__main__.TestStringMethods) ... ok
    test_upper (__main__.TestStringMethods) ... ok
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.001s
    
    OK
    

    通过像这样使用单元测试发现:

    ~roottwo\projects> python -m leveltest --level 2 -v
    test_isupper (testproject.TestStringMethods) ... skipped 'test level too low.'
    test_split (testproject.TestStringMethods) ... skipped 'test level too low.'
    test_upper (testproject.TestStringMethods) ... skipped 'test level too low.'
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.003s
    
    OK (skipped=3)
    
    ~roottwo\projects> python -m leveltest --level 3 discover -v
    test_isupper (testproject.TestStringMethods) ... ok
    test_split (testproject.TestStringMethods) ... skipped 'test level too low.'
    test_upper (testproject.TestStringMethods) ... skipped 'test level too low.'
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.001s
    
    OK (skipped=2)
    
    ~roottwo\projects> python -m leveltest --level 4 -v
    test_isupper (testproject.TestStringMethods) ... ok
    test_split (testproject.TestStringMethods) ... ok
    test_upper (testproject.TestStringMethods) ... skipped 'test level too low.'
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.001s
    
    OK (skipped=1)
    
    ~roottwo\projects> python -m leveltest discover --level 5 -v
    test_isupper (testproject.TestStringMethods) ... ok
    test_split (testproject.TestStringMethods) ... ok
    test_upper (testproject.TestStringMethods) ... ok
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.001s
    
    OK
    

    或者通过指定要运行的测试用例,例如:

    ~roottwo\projects>python -m leveltest --level 3 testproject -v
    test_isupper (testproject.TestStringMethods) ... ok
    test_split (testproject.TestStringMethods) ... skipped 'test level too low.'
    test_upper (testproject.TestStringMethods) ... skipped 'test level too low.'
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.002s
    
    OK (skipped=2)
    

    【讨论】:

    • 所以,感谢您的回答,但我不知道这是否允许 discover 能够遍历目录中的所有文件,然后为所有他们。
    • 它使用unittest 进行所有测试。所以,是的,它提供与unittest 相同的报告。我的答案中的示例使用 -v(详细)标志进行单元测试,以提供有关所有测试的详细信息,包括哪些测试因测试级别太低而被跳过。
    • 啊,我明白了——很有趣。感谢您的回答 - 会再看一些。赞赏!
    • 非常感谢您的回答。我希望我也能将赏金奖励给你。不幸的是,网站规则不允许添加赏金点,或拆分它们。否则,我会很高兴地这样做。一切顺利。
    猜你喜欢
    • 2016-06-19
    • 2012-07-07
    • 2023-01-06
    • 2018-10-17
    • 2021-10-23
    • 2018-03-07
    • 2018-11-19
    • 2016-09-08
    • 2018-06-27
    相关资源
    最近更新 更多