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


315

我有一个目录,其中包含我的Python单元测试。每个单元测试模块的形式为test _ *。py。我正在尝试制作一个名为all_test.py的文件,您猜对了,它将以上述测试形式运行所有文件并返回结果。到目前为止,我已经尝试了两种方法。都失败了。我将展示这两种方法,并希望那里的人知道如何正确地正确执行此操作。

对于我的第一次英勇尝试,我想:“如果我只是将所有测试模块导入文件中,然后调用此unittest.main()doodad,它将起作用,对吗?” 好吧,原来我错了。

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,但这是进步。但是如何传递结果并将其显示给main?还是我基本上如何使它工作,以便我可以运行该文件,然后运行此目录中的所有单元测试?


1
如果您使用的是Python 2.7+,请跳至Travis的答案
Rocky

您是否曾经尝试过从测试实例对象运行测试?
Pinocchio

有关示例文件结构的解决方案,请参见此答案
Derek Soike

Answers:


477

使用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 unittest文档中阅读更多内容。


11
问题包括:ImportError:起始目录不可导入:
zinking

6
至少在Linux上使用Python 2.7.8时,命令行调用都不给我递归。我的项目有几个子项目,它们的单元测试分别位于“ unit_tests / <subproject> / python /”目录中。如果我指定了这样的路径,则将运行该子项目的单元测试,但是仅使用“ unit_tests”作为测试目录参数,就不会找到测试(而不是我希望的所有子项目的所有测试)。有什么提示吗?
user686249

6
关于递归:第一个不带<test_directory>的命令默认为“”。并递归到子模块。也就是说,要发现的所有测试目录都必须具有init .py。如果这样做,将通过发现命令找到它们。刚刚尝试过,它奏效了。
EmilStenström,2016年

这对我有用。我有一个包含四个文件的测试文件夹,可以在我的Linux终端上运行,非常棒。
JasTonAChair

5
谢谢!为什么这不是公认的答案?在我看来,更好的答案永远是不需要任何外部依赖的答案……
Jonathan Benn

108

您可以使用可以为您完成此任务的测试运行程序。 例如鼻子很好。运行时,它将在当前树中找到测试并运行它们。

更新:

这是我前鼻时期的一些代码。您可能不希望使用模块名称的明确列表,但是其余的名称可能对您有用。

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()相比,这种方法的优势是您可以选择在某些模块而不是其他模块中声明一个测试套件吗?
科里·波特2009年

1
我试了一下鼻子,效果很好。在我的项目中安装和运行都很容易。我什至可以在virtualenv内运行几行脚本来实现自动化。鼻子+1!
杰西·韦伯

并非总是可行的:有时,如果项目的导入结构试图在模块上运行导入,则可能导致混乱。
2015年

4
请注意,鼻子已经“过去几年处于维护模式”,目前建议对新项目使用鼻子2pytest或仅使用普通unittest / unittest2
Kurt Peek

您是否曾经尝试过从测试实例对象运行测试?
Pinocchio

95

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

  • 您的目录中必须有一个空(或其他)__init__.py文件test必须命名为test/
  • 您的测试文件test/与模式匹配test_*.py。它们可以位于下方的子目录中test/,并且这些子目录可以命名为任何东西。

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

python -m unittest

做完了!解决方案少于100行。希望其他python初学者可以通过查找此方法节省时间。


3
请注意,默认情况下,它仅搜索以“ test”开头的文件名中的测试
Shawabawa

3
没错,原始问题涉及“每个单元测试模块的形式为test _ *。py。”的事实,因此可以直接答复此答案。现在我已经更新了答案更加明确
tmck码

1
谢谢,这是我使用Travis Bear的答案所缺少的。
杰里米·科乔伊

65

现在,可以直接从unittest: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)

3
我也尝试过这种方法,进行了几次测试,但是效果很好。优秀的!!!但是我很好奇我只有4个测试。它们一起运行0.032,但是当我使用此方法全部运行时,我得到结果,.... ---------------------------------------------------------------------- Ran 4 tests in 0.000s OK为什么?区别,它来自哪里?
simkus '18

我无法从命令行运行看起来像这样的文件。应该如何调用它?
达斯汀·米歇尔

python file.py
slaughter98年

1
完美地工作!只需在您的test /目录中进行设置,然后设置start_id =“ ./”即可。恕我直言,这个答案现在(Python 3.7)已被接受!
jjwdesign

您可以将最后一行更改为´res = Runner.run(suite); sys.exit(如果res.wasSuccessful()则为0,否则为1)´,如果您想要正确的退出代码
Sadap

32

通过研究上面的代码(特别是使用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)

是的,使用鼻子可能比这样做更容易,但这不重要。


好,它对于当前目录工作正常,如何直接调用子目录?
拉里·蔡

拉里(Larry),请参阅递归测试发现的新答案(stackoverflow.com/a/24562019/104143)–
彼得·科夫勒

您是否曾经尝试过从测试实例对象运行测试?
Pinocchio

25

如果要运行各种测试用例类中的所有测试,并且很乐意明确指定它们,则可以这样做:

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我的项目在哪里,TestSymbols并且TestPatterns是的子类TestCase


来自unittest.TestLoader文档:“通常,不需要创建此类的实例; unittest模块提供了一个可以作为unittest.defaultTestLoader共享的实例。” 同样,由于TestSuite接受iterable作为参数,因此您可以在循环中构造该iterable以避免重复loader.loadTestsFromTestCase
两位炼金术士

@ Two-Bit Alchemist,您的第二点特别好。我将代码更改为包括,但无法对其进行测试。(虽然我意识到我很不理智(给他们拧了个驼峰大小写变量名称),但第一个mod会让它看起来太像Java。
痴呆的刺猬

这是我的最爱,很干净。能够将其打包并在我的常规命令行中将其作为参数。
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,我想
拉里·蔡

@larrycai也许,我通常使用Python 3,有时使用Python 2.7。该问题与特定版本无关。
rds

我使用Python 3.4,发现返回了一个套件,从而使循环变得多余。
沙丘2014年

对于未来的Larry来说:“ Python 2.7在unittest中添加了许多新功能,包括测试发现。unittest2允许您将这些功能与Python的早期版本一起使用。”
两位炼金术士

8

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

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

最重要的是,它肯定有效。


7

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

要使用此命令,您的项目的测试必须unittest通过函数,TestCase类或方法或包含TestCase类的模块或包来包装在测试套件中。如果命名套件是一个模块,并且该模块具有一个additional_tests()功能,则将其调用,并将结果(必须为unittest.TestSuite)添加到要运行的测试中。如果命名套件是一个包,则将所有子模块和子包递归地添加到整个测试套件中

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

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

然后跑 python setup.py test

除非您避免在测试套件中进行相对导入,否则在Python 3中基于文件的发现可能会出现问题,因为会discover使用文件导入。即使它支持optional top_level_dir,但是我还是有一些无限递归错误。因此,针对非打包代码的简单解决方案是将以下内容放入__init__.py测试包中(请参阅load_tests协议)。

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

很好的答案,它可用于在部署之前自动执行测试!谢谢
亚瑟·克勒克·格拉迪

4

我使用PyDev / LiClipse,但还没有真正弄清楚如何从GUI一次运行所有测试。(编辑:右键单击根测试文件夹,然后选择Run as -> Python unit-test

这是我当前的解决方法:

import unittest

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

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

我把这段代码放在一个名为 all测试目录中。如果我以LiClipse的单元测试形式运行此模块,则将运行所有测试。如果我只要求重复特定或失败的测试,则仅运行那些测试。它也不会干扰我的命令行测试运行程序(noestests)-被忽略。

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


所有测试文件和测试方法的名称均应以“ test_”开头。否则,命令“运行方式-> Python单元测试”将找不到它们。
Stefan

2

根据Stephen Cagle的回答,我添加了对嵌套测试模块的支持。

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


2

这是一个老问题,但现在(2019年)对我有用的是:

python -m unittest *_test.py

我所有的测试文件都与源文件位于同一文件夹中,并以结尾_test



1

无论您位于哪个工作目录中,此BASH脚本都将从文件系统中的任何位置执行python unittest测试目录:其工作目录始终test位于该目录所在的位置。

所有测试,独立$ PWD

unittest Python模块对您的当前目录敏感,除非您告诉它当前位置(使用discover -s选项)。

当位于./src./example工作目录中并且需要快速的整体单元测试时,这很有用:

#!/bin/bash
this_program="$0"
dirname="`dirname $this_program`"
readlink="`readlink -e $dirname`"

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

所选测试,独立$ PWD

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

runone.py <test-python-filename-minus-dot-py-fileextension>
#!/bin/bash
this_program="$0"
dirname="`dirname $this_program`"
readlink="`readlink -e $dirname`"

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

test/__init__.py在生产过程中不需要文件来负担您的程序包/内存开销。


-3

这是我创建包装器以从命令行运行测试的方法:

#!/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类,因此每个测试都将看起来像:

from BaseTest import BaseTest
class FooTestPagesBasic(BaseTest):
    def test_foo(self):
        driver = self.driver
        driver.get(self.base_url + "/")

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

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

2
这个答案的大部分与测试发现(即日志记录等)无关。堆栈溢出用于回答问题,而不是炫耀不相关的代码。
科里·戈德堡
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.