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

471
我有一个包含Python单元测试的目录。每个单元测试模块的形式为test_*.py。我正在尝试创建一个名为all_test.py的文件,它将运行上述测试形式中的所有文件并返回结果。到目前为止,我已经尝试了两种方法; 但都失败了。我将展示这两种方法,希望有人知道如何正确地完成这个任务。
对于我的第一次勇敢的尝试,我想:“如果我只是在文件中导入所有的测试模块,然后调用unittest.main()这个东西,它会工作,对吧?” 好吧,结果证明我错了。
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()

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

$ python all_test.py 

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

OK

为了我的第二次尝试,我想,好吧,也许我可以尝试以一种更“手动”的方式完成整个测试过程。所以我尝试在下面这样做:

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()

这个方法也没有成功,但是它似乎离成功很近了!

$ 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,但这已经有进展了。但是我该如何传递和显示结果给主函数?或者说,我该如何让它正常运行,以便我只需运行此文件,并在此过程中运行此目录中的所有单元测试?


你是否曾经尝试过从测试实例对象中运行测试? - Charlie Parker
请参考此答案,其中提供了一个带有示例文件结构的解决方案。 - Derek Soike
18个回答

714

使用 Python 2.7 及更高版本,您无需编写新代码或使用第三方工具即可执行递归测试;命令行通过内置方式支持。将 __init__.py 放置在您的测试目录中,并运行以下命令:

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

您可以在Python 2.7Python 3.x的单元测试文档中了解更多信息。


16
问题包括:ImportError:起始目录不可导入: - zinking
6
至少在Linux上,Python 2.7.8版本中,无论是哪种命令行调用方式都不会出现递归。我的项目有几个子项目,它们的单元测试放在各自的"unit_tests/<subproject>/python/"目录下。如果我指定这样的路径,则会运行该子项目的单元测试,但是如果只使用"unit_tests"作为测试目录参数,则找不到任何测试(而不是像我希望的那样找到所有子项目的所有测试)。有什么提示吗? - user686249
6
关于递归:第一个没有指定<test_directory>的命令会默认为“。”并递归到子模块。也就是说,您想要发现的所有测试目录都需要有一个__init__.py文件。如果有这个文件,它们将被discover命令发现。我刚刚尝试过,它有效。 - Emil Stenström
2
即使我没有指定 -s 和 -p 参数,这对我仍然有效。只需编写 python -m unittest discover - qurban
我的测试似乎需要使用“-m pytest”来运行。测试函数在测试文件中定义,但没有显式的函数调用。如果我尝试“unittest discover”,它会发现零个测试。 - Yan King Yin
显示剩余3条评论

270

在Python 3中,如果您正在使用unittest.TestCase

  • 您必须在 test 目录下拥有一个空(或其他)__init__.py 文件(必须命名为test/)。
  • 您位于 test/ 内部的测试文件匹配模式为test_*.py
    • 它们可以在test/的子目录中。这些子目录可以任意命名,但是它们都需要有一个__init__.py文件。

然后,您可以运行所有测试:

python -m unittest

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


12
请注意,默认情况下它只会在文件名以“test”开头的测试中搜索。 - Shawabawa
4
没问题,原始问题提到了“每个单元测试模块的形式都是test_*.py。”这一事实,因此这个回答是直接回复该问题。我已经更新了答案,使其更加明确易懂。 - tmck-code
5
要让它正常运作,我还需要在每个子文件夹中添加__init__.py文件,否则就很好。谢谢! - nope
3
您能否更新您的答案,包括子目录也需要作为包处理,这意味着您需要在测试目录内的子目录中添加一个 init.py 文件?请注意修改后的内容应保持原意,通俗易懂。 - eager2learn
1
自从unittest版本3.2以后,你也可以使用新的关键字discover来发现所有现有的单元测试:python -m unittest discover - R Yoda
显示剩余5条评论

111

你可以使用一个测试运行器来帮助你做到这一点。例如,nose非常好用。运行时,它会在当前目录中查找测试并运行它们。

更新:

这是我在使用nose之前的一些代码。你可能不需要显式列出模块名称,但其余部分可能对你有用。

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)

2
这种方法相对于将所有测试模块明确导入到一个test_all.py模块中并调用unittest.main()的优势在于,您可以选择在某些模块中声明测试套件而在其他模块中不声明。 - Corey Porter
1
我尝试了nose,它完美地运行了。安装和在我的项目中运行都很容易。我甚至能够用几行脚本自动化它,在虚拟环境中运行。为nose加1分! - Jesse Webb
并非总是可行的:有时导入项目结构可能会导致nose在尝试运行模块上的导入时感到困惑。 - chiffa
6
请注意,nose 已经“处于维护模式数年”,目前建议使用 nose2pytest 或者纯粹的 unittest / unittest2 来开展新项目。 - Kurt Peek
你是否曾经尝试过从测试实例对象中运行测试? - Charlie Parker

91

现在可以直接使用unittest.TestLoader.discover从单元测试中实现这一点:unittest.TestLoader.discover

import unittest
loader = unittest.TestLoader()
start_dir = 'path/to/your/test/files'
suite = loader.discover(start_dir)

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

4
我也尝试了这种方法,进行了几次测试,效果非常好。太棒了!但我很好奇,我只有4个测试,它们一起运行需要0.032秒,但当我使用这种方法运行它们时,我得到的结果是“Ran 4 tests in 0.000s,OK”。为什么会这样?差异从哪里来? - simkusr
我在命令行中运行一个看起来像这样的文件时遇到了问题。应该如何调用它? - Dustin Michels
python file.py - slaughter98
2
运行得非常完美!只需将其设置在您的测试/目录中,然后将start_id =“./”设置即可。在我看来,这个答案现在(Python 3.7)是被接受的方式! - jjwdesign
2
如果您想要正确的退出代码,您可以将最后一行更改为´res = runner.run(suite); sys.exit(0 if res.wasSuccessful() else 1)´。 - bb1950328

33

通过对上面的代码进行一些研究(特别是使用 TextTestRunnerdefaultTestLoader),我已经接近成功了。最终,我通过将所有测试套件传递给单个套件构造函数,而不是“手动”添加它们,从而解决了我的其他问题。因此,以下是我的解决方案。

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)

是的,可能只需要使用nose比这样做更容易,但那并不是重点。


很好,它在当前目录下运行良好,如何调用子目录? - Larry Cai
Larry,请查看递归测试发现的新答案(https://dev59.com/VXI-5IYBdhLWcg3wq6do#24562019)。 - Peter Kofler
你有没有尝试过从测试实例对象中运行测试? - Charlie Parker

29

如果你想运行来自不同测试用例类的所有测试,并且你乐意明确指定它们,那么你可以这样做:

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是我的项目,而TestSymbolsTestPatterns则是继承自TestCase的子类。


unittest.TestLoader 文档 中可以看到:"通常情况下,不需要创建此类的实例;unittest 模块提供了一个可共享的实例,即 unittest.defaultTestLoader。" 另外,由于 TestSuite 接受一个 iterable 作为参数,因此您可以在循环中构造该可迭代对象,以避免重复使用 loader.loadTestsFromTestCase - Two-Bit Alchemist
@Two-Bit Alchemist,你的第二点意见特别好。 我会更改代码以包括它,但我无法测试它。(第一种修改方式让它看起来太像Java了,我不喜欢.. 尽管我意识到我有些不理性(他们和他们的驼峰命名变量可以去死))。 - demented hedgehog
这是我的最爱,非常干净。我能够将其打包并将其作为我的常规命令行参数。 - MarkII

15

我使用了discover方法和对load_tests的重载来在(我认为)最少的行数中实现了这个结果:

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()

执行五个东西之类的操作

Ran 27 tests in 0.187s
OK

这仅适用于Python2.7,我猜。 - Larry Cai
@larrycai 或许吧,我通常使用Python 3,有时也会用Python 2.7。这个问题并没有与特定版本绑定。 - rds
我正在使用Python 3.4,发现返回一个suite,使循环变得多余。 - Dunes
对于未来的Larry们:“Python 2.7中添加了许多新功能,包括测试发现。unittest2允许您在早期版本的Python中使用这些功能。” - Two-Bit Alchemist

9

我尝试了各种方法,但都存在一些缺陷或者需要编写一些代码,这真的很烦人。不过在 Linux 下有一个方便的方法,就是通过某种模式查找每个测试,并逐个调用它们。

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

最重要的是,它绝对有效。


8

如果你使用的是一个已打包好的库或应用程序,你不需要这样做。 setuptools 会为你完成这项工作

要使用此命令,您的项目测试必须由函数、TestCase类或方法或包含TestCase类的模块或包封装在unittest测试套件中。如果指定的测试套件是一个模块,并且该模块有一个additional_tests()函数,则会调用它,并将结果(必须是unittest.TestSuite)添加到要运行的测试中。如果指定的测试套件是一个包,则任何子模块和子包都将递归地添加到总体测试套件中。

只需告诉它您的根测试包所在的位置,例如:

setup(
    # ...
    test_suite = 'somepkg.test'
)

运行python setup.py test

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

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

5

这是一个旧问题,但在我现在(2019年)有效的方法是:

python -m unittest *_test.py

所有的测试文件都与源文件在同一个文件夹中,并且它们以_test结尾。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接