代码之家  ›  专栏  ›  技术社区  ›  Ami Tavory

通过unittest发现传递参数(用于argparse)

  •  18
  • Ami Tavory  · 技术社区  · 10 年前

    foo 是一个具有深度目录嵌套的Python项目,包括~30 unittest 不同子目录中的文件。在内部 foo公司 的 setup.py ,我已经 added a custom "test" command 内部运行的

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

    请注意,这使用 unittest 's discovery 模式


    由于有些测试非常慢,我最近决定测试应该有“级别”。答案是 this question 很好地解释了如何获得 单元测试 和 argparse 相互合作。现在,我可以运行 个人 比如unittest文件 foo/bar/_bar_test.py 具有

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

    并且仅运行3级测试。

    问题是我无法知道如何使用discover传递自定义标志(在本例中为“--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. 它可以以某种方式支持多个测试级别(使用问题中的技术或使用其他不同的机制)。
    3 回复  |  直到 9 年前
        1
  •  7
  •   RootTwo    10 年前

    这不会使用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)
    

    下面是您如何在示例测试项目中使用它。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)
    
        2
  •  7
  •   Peter Brittain    10 年前

    您的问题是,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)
    

    现在是解析器。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
    
        3
  •  6
  •   ikhtiyor    10 年前

    使用discover时无法传递参数。 DiscoveringTestLoader 类,删除所有不匹配的文件(使用“*test.py--level=3”消除),并只将文件名传递到 unittest.TextTestRunner

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

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