如何在目录中运行所有Python单元测试?

How do I run all Python unit tests in a directory?

我有一个包含Python单元测试的目录。每个单元测试模块都是形式测试。我正试图创建一个名为all_test.py的文件,您猜对了,它将运行上述测试表单中的所有文件并返回结果。到目前为止,我试过两种方法,都失败了。我将展示这两种方法,我希望有人知道如何正确地做到这一点。

对于我的第一次勇敢尝试,我想,"如果我只是导入文件中的所有测试模块,然后调用这个unittest.main()doodad,它会工作的,对吗?"结果发现我错了。

1
2
3
4
5
6
7
8
9
import glob
import unittest

testSuite = unittest.TestSuite()
test_file_strings = glob.glob('test_*.py')
module_strings = [str[0:len(str)-3] for str in test_file_strings]

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

这不起作用,我得到的结果是:

1
2
3
4
5
6
$ python all_test.py

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

对于我的第二次尝试,我想,好吧,也许我会尝试用一种更"手动"的方式来做整个测试。所以我尝试在下面这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import glob
import unittest

testSuite = unittest.TestSuite()
test_file_strings = glob.glob('test_*.py')
module_strings = [str[0:len(str)-3] for str in test_file_strings]
[__import__(str) for str in module_strings]
suites = [unittest.TestLoader().loadTestsFromName(str) for str in module_strings]
[testSuite.addTest(suite) for suite in suites]
print testSuite

result = unittest.TestResult()
testSuite.run(result)
print result

#Ok, at this point I have a result
#How do I display it as the normal unit test command line output?
if __name__ =="__main__":
    unittest.main()

这也不起作用,但似乎如此接近!

1
2
3
4
5
6
7
8
$ python all_test.py
<unittest.TestSuite tests=[<unittest.TestSuite tests=[<unittest.TestSuite tests=[<test_main.TestMain testMethod=test_respondes_to_get>]>]>]>
<unittest.TestResult run=1 errors=0 failures=0>

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

我似乎有某种类型的套件,我可以执行结果。我有点担心它说我只有run=1,看起来应该是run=2,但这是进步。但是如何将结果传递并显示给main呢?或者,我基本上如何让它工作,这样我就可以运行这个文件,并且在这样做时,运行这个目录中的所有单元测试?


对于Python2.7及更高版本,您不必编写新代码或使用第三方工具来完成这项工作;通过命令行进行递归测试是内置的。

1
2
3
python -m unittest discover <test_directory>
# or
python -m unittest discover -s <directory> -p '*_test.py'

您可以在python 2.7中阅读更多内容或python 3.x unittest文档。


您可以使用一个测试运行程序来完成这项工作。例如,鼻子很好。运行时,它将在当前树中找到测试并运行它们。

更新:

这是我鼻子前几天的一些代码。您可能不需要模块名称的显式列表,但其余的可能对您有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
testmodules = [
    'cogapp.test_makefiles',
    'cogapp.test_whiteutils',
    'cogapp.test_cogapp',
    ]

suite = unittest.TestSuite()

for t in testmodules:
    try:
        # If the module defines a suite() function, call it to get the suite.
        mod = __import__(t, globals(), locals(), ['suite'])
        suitefn = getattr(mod, 'suite')
        suite.addTest(suitefn())
    except (ImportError, AttributeError):
        # else, just load all the test cases from the module.
        suite.addTest(unittest.defaultTestLoader.loadTestsFromName(t))

unittest.TextTestRunner().run(suite)


现在可以直接从unittest:unittest.testloader.discover进行此操作。

1
2
3
4
5
6
7
import unittest
loader = unittest.TestLoader()
start_dir = 'path/to/your/test/files'
suite = loader.discover(start_dir)

runner = unittest.TextTestRunner()
runner.run(suite)


在python 3中,如果您使用的是unittest.TestCase

    百万千克1您的test目录中必须有一个空(或其他)的__init__.py文件(必须命名为test/)百万千克1百万千克1您在test/中的测试文件与模式test_*.py匹配。它们可以在test/下的子目录中,并且这些子目录可以命名为任何内容。百万千克1

然后,可以使用以下命令运行所有测试:

1
python -m unittest

完成!少于100行的解决方案。希望另一个Python初学者通过找到这个来节省时间。


通过研究上面的代码(特别是使用TextTestRunnerdefaultTestLoader),我可以非常接近。最后,我通过将所有测试套件传递给单个套件构造函数来修复代码,而不是"手动"添加它们,这解决了我的其他问题。这是我的解决方案。

1
2
3
4
5
6
7
8
import glob
import unittest

test_files = glob.glob('test_*.py')
module_strings = [test_file[0:len(test_file)-3] for test_file in test_files]
suites = [unittest.defaultTestLoader.loadTestsFromName(test_file) for test_file in module_strings]
test_suite = unittest.TestSuite(suites)
test_runner = unittest.TextTestRunner().run(test_suite)

是的,仅仅用鼻子可能比用鼻子更容易,但这也不重要。


如果您想要运行来自不同测试用例类的所有测试,并且您很乐意显式地指定它们,那么您可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from unittest import TestLoader, TextTestRunner, TestSuite
from uclid.test.test_symbols import TestSymbols
from uclid.test.test_patterns import TestPatterns

if __name__ =="__main__":

    loader = TestLoader()
    tests = [
        loader.loadTestsFromTestCase(test)
        for test in (TestSymbols, TestPatterns)
    ]
    suite = TestSuite(tests)

    runner = TextTestRunner(verbosity=2)
    runner.run(suite)

其中uclid是我的项目,TestSymbolsTestPatternsTestCase的子类。


我已经使用了discover方法和load_tests的重载来实现这一结果(我认为是最小的)代码行数:

1
2
3
4
5
6
7
8
9
10
11
def load_tests(loader, tests, pattern):
''' Discover and load all unit tests in all files named ``*_test.py`` in ``./src/``
'''

    suite = TestSuite()
    for all_test_suite in unittest.defaultTestLoader.discover('src', pattern='*_tests.py'):
        for test_suite in all_test_suite:
            suite.addTests(test_suite)
    return suite

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

在五个人身上执行类似

1
2
Ran 27 tests in 0.187s
OK


我尝试了各种各样的方法,但似乎都有缺陷,或者我必须编写一些代码,这很烦人。但是在Linux下有一种方便的方法,那就是通过特定的模式找到每个测试,然后逐个调用它们。

1
find . -name 'Test*py' -exec python '{}' \;

最重要的是,它确实有效。


对于打包的库或应用程序,您不想这样做。我会帮你的。

To use this command, your project’s tests must be wrapped in a unittest test suite by either a function, a TestCase class or method, or a module or package containing TestCase classes. If the named suite is a module, and the module has an additional_tests() function, it is called and the result (which must be a unittest.TestSuite) is added to the tests to be run. If the named suite is a package, any submodules and subpackages are recursively added to the overall test suite.

只需告诉它您的根测试包在哪里,比如:

1
2
3
4
setup(
    # ...
    test_suite = 'somepkg.test'
)

运行python setup.py test

在python 3中,基于文件的发现可能有问题,除非避免在测试套件中进行相对导入,因为discover使用文件导入。尽管它支持可选的top_level_dir,但我有一些无限递归错误。因此,对于非打包代码,一个简单的解决方案是将以下内容放在测试包的__init__.py中(请参见加载测试协议)。

1
2
3
4
5
6
7
8
9
10
11
import unittest

from . import foo, bar


def load_tests(loader, tests, pattern):
    suite = unittest.TestSuite()
    suite.addTests(loader.loadTestsFromModule(foo))
    suite.addTests(loader.loadTestsFromModule(bar))

    return suite


我使用pydev/liclipse,还没有真正了解如何从GUI一次运行所有测试。(编辑:右击根测试文件夹,选择Run as -> Python unit-test

这是我目前的解决方法:

1
2
3
4
5
6
7
import unittest

def load_tests(loader, tests, pattern):
    return loader.discover('.')

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

我把这个代码放在我的测试目录中一个名为all的模块中。如果我从Liclipse将此模块作为UnitTest运行,则所有测试都将运行。如果我只要求重复特定的或失败的测试,那么只运行那些测试。它也不会干扰我的命令行测试运行程序(nosetest)——它被忽略了。

您可能需要根据项目设置将参数更改为discover


这个bash脚本将从文件系统中的任何地方执行python unittest目录,不管您在哪个工作目录中:它的工作目录总是位于该test目录所在的位置。

所有测试,独立$pwd

UnitTest python模块对当前目录很敏感,除非您告诉它在哪里(使用discover -s选项)。

当您停留在./src./example工作目录中时,这很有用,您需要快速的整体单元测试:

1
2
3
4
5
6
#!/bin/bash
this_program="$0"
dirname="`dirname $this_program`"
readlink="`readlink -e $dirname`"

python -m unittest discover -s"$readlink"/test -v

选定测试,独立$pwd

我将此实用程序文件命名为:runone.py,并按如下方式使用:

1
runone.py <test-python-filename-minus-dot-py-fileextension>

1
2
3
4
5
6
#!/bin/bash
this_program="$0"
dirname="`dirname $this_program`"
readlink="`readlink -e $dirname`"

(cd"$dirname"/test; python -m unittest $1)

不需要使用test/__init__.py文件来负担生产期间的包/内存开销。


基于StephenCagle的答案,我增加了对嵌套测试模块的支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import fnmatch
import os
import unittest

def all_test_modules(root_dir, pattern):
    test_file_names = all_files_in(root_dir, pattern)
    return [path_to_module(str) for str in test_file_names]

def all_files_in(root_dir, pattern):
    matches = []

    for root, dirnames, filenames in os.walk(root_dir):
        for filename in fnmatch.filter(filenames, pattern):
            matches.append(os.path.join(root, filename))

    return matches

def path_to_module(py_file):
    return strip_leading_dots( \
        replace_slash_by_dot(  \
            strip_extension(py_file)))

def strip_extension(py_file):
    return py_file[0:len(py_file) - len('.py')]

def replace_slash_by_dot(str):
    return str.replace('\', '.').replace('/', '.')

def strip_leading_dots(str):
    while str.startswith('
.'):
       str = str[1:len(str)]
    return str

module_names = all_test_modules('
.', '*Tests.py')
suites = [unittest.defaultTestLoader.loadTestsFromName(mname) for mname
    in module_names]

testSuite = unittest.TestSuite(suites)
runner = unittest.TextTestRunner(verbosity=1)
runner.run(testSuite)

代码搜索.的所有子目录以查找随后加载的*Tests.py文件。它期望每个*Tests.py包含一个单独的类*Tests(unittest.TestCase),该类依次加载并执行。

这适用于任意深度嵌套目录/模块,但其中的每个目录至少需要包含一个空的__init__.py文件。这允许测试通过用点替换斜杠(或反斜杠)来加载嵌套模块(参见replace_slash_by_dot)。


因为测试发现似乎是一个完整的主题,所以有一些专用的测试发现框架:

  • 鼻子
  • P.试验

更多信息请阅读:https://wiki.python.org/moin/pythontestingtoolstaxonomy


下面是我的方法,通过创建一个包装器来从命令行运行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/usr/bin/env python3
import os, sys, unittest, argparse, inspect, logging

if __name__ == '__main__':
    # Parse arguments.
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-?","--help",     action="help",                        help="show this help message and exit" )
    parser.add_argument("-v","--verbose",  action="store_true", dest="verbose",  help="increase output verbosity" )
    parser.add_argument("-d","--debug",    action="store_true", dest="debug",    help="show debug messages" )
    parser.add_argument("-h","--host",     action="store",      dest="host",     help="Destination host" )
    parser.add_argument("-b","--browser",  action="store",      dest="browser",  help="Browser driver.", choices=["Firefox","Chrome","IE","Opera","PhantomJS"] )
    parser.add_argument("-r","--reports-dir", action="store",   dest="dir",      help="Directory to save screenshots.", default="reports")
    parser.add_argument('files', nargs='*')
    args = parser.parse_args()

    # Load files from the arguments.
    for filename in args.files:
        exec(open(filename).read())

    # See: http://codereview.stackexchange.com/q/88655/15346
    def make_suite(tc_class):
        testloader = unittest.TestLoader()
        testnames = testloader.getTestCaseNames(tc_class)
        suite = unittest.TestSuite()
        for name in testnames:
            suite.addTest(tc_class(name, cargs=args))
        return suite

    # Add all tests.
    alltests = unittest.TestSuite()
    for name, obj in inspect.getmembers(sys.modules[__name__]):
        if inspect.isclass(obj) and name.startswith("FooTest"):
            alltests.addTest(make_suite(obj))

    # Set-up logger
    verbose = bool(os.environ.get('VERBOSE', args.verbose))
    debug   = bool(os.environ.get('DEBUG', args.debug))
    if verbose or debug:
        logging.basicConfig( stream=sys.stdout )
        root = logging.getLogger()
        root.setLevel(logging.INFO if verbose else logging.DEBUG)
        ch = logging.StreamHandler(sys.stdout)
        ch.setLevel(logging.INFO if verbose else logging.DEBUG)
        ch.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(name)s: %(message)s'))
        root.addHandler(ch)
    else:
        logging.basicConfig(stream=sys.stderr)

    # Run tests.
    result = unittest.TextTestRunner(verbosity=2).run(alltests)
    sys.exit(not result.wasSuccessful())

为了简单起见,请原谅我的非PEP8编码标准。

然后,您可以为所有测试的公共组件创建basetest类,因此您的每个测试看起来都很简单:

1
2
3
4
5
from BaseTest import BaseTest
class FooTestPagesBasic(BaseTest):
    def test_foo(self):
        driver = self.driver
        driver.get(self.base_url +"/")

要运行,只需将测试指定为命令行参数的一部分,例如:

1
./run_tests.py -h http://example.com/ tests/**/*.py