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

Posted

技术标签:

【中文标题】如何在目录中运行所有 Python 单元测试?【英文标题】:How do I run all Python unit tests in a directory? 【发布时间】:2010-12-16 11:44:02 【问题描述】:

我有一个目录,其中包含我的 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?或者我如何基本上让它工作,这样我就可以运行这个文件,然后运行这个目录中的所有单元测试?

【问题讨论】:

如果您使用的是 Python 2.7+,请跳至 Travis 的回答 您是否尝试过从测试实例对象运行测试? 请参阅this answer 以获取具有示例文件结构的解决方案。 【参考方案1】:

我没有包,如本页所述,这在发布发现时会产生问题。所以,我使用了以下解决方案。所有的测试结果都将放在给定的输出文件夹中。

RunAllUT.py

"""
The given script is executing all the Unit Test of the project stored at the
path %relativePath2Src% currently fixed coded for the given project. 

Prerequired:
    - Anaconda should be install
    - For the current user, an enviornment called "mtToolsEnv" should exists
    - xmlrunner Library should be installed
"""

import sys
import os
import xmlrunner
from Repository import repository 

relativePath2Src="./../.."
pythonPath=r'"C:\Users\%USERNAME%\.conda\envs\YourConfig\python.exe"' 
outputTestReportFolder=os.path.dirname(os.path.abspath(__file__))+r'\test-reports' #subfolder in current file path

class UTTesting():
    """
    Class tto run all the UT of the project
    """
    def __init__(self):
        """
        Initiate instance

        Returns
        -------
        None.

        """
        self.projectRepository = repository() 
        self.UTfile = [] #List all file
    
    def retrieveAllUT(self):
        """
        Generate the list of UT file in the project

        Returns
        -------
        None.

        """
        print(os.path.realpath(relativePath2Src))
        self.projectRepository.retriveAllFilePaths(relativePath2Src)
        #self.projectRepository.printAllFile() #debug
        for file2scan in self.projectRepository.devfile:
            if file2scan.endswith("_UT.py"):
                self.UTfile.append(file2scan)
                print(self.projectRepository.devfilepath[file2scan]+'/'+file2scan)
                
    
    def runUT(self,UTtoRun):
        """
        Run a single UT

        Parameters
        ----------
        UTtoRun : String
            File Name of the UT

        Returns
        -------
        None.

        """
        print(UTtoRun)
        if UTtoRun in self.projectRepository.devfilepath:
            UTtoRunFolderPath=os.path.realpath(os.path.join(self.projectRepository.devfilepath[UTtoRun]))
            UTtoRunPath = os.path.join(UTtoRunFolderPath, UTtoRun)
        print(UTtoRunPath)
        
        #set the correct execution context & run the test
        os.system(" cd " + UTtoRunFolderPath + \
                  " & " + pythonPath + " " + UTtoRunPath + " " + outputTestReportFolder )
        
        
    def runAllUT(self):
        """
        Run all the UT contained in self
        The function "retrieveAllUT" sjould ahve been performed before

        Returns
        -------
        None.

        """
        for UTfile in self.UTfile:
            self.runUT(UTfile)
                
    
                
if __name__ == "__main__":
    undertest=UTTesting()
    undertest.retrieveAllUT()
    undertest.runAllUT()

在我的特定项目中,我有一个在其他脚本中使用的类。这对您的用例来说可能有点过头了。

Repository.py

import os

class repository():
    """
    Class that decribed folder and file in a repository 
    """
    def __init__(self):
        """
        Initiate instance

        Returns
        -------
        None.

        """
        self.devfile = [] #List all file
        self.devfilepath =  #List all file paths

    def retriveAllFilePaths(self,pathrepo):
        """
        Retrive all files and their path in the class

        Parameters
        ----------
        pathrepo : Path used for the parsin

        Returns
        -------
        None.

        """
        for path, subdirs, files in os.walk(pathrepo):
            for file_name in files:
                self.devfile.append(file_name)
                self.devfilepath[file_name] = path
                
    def printAllFile(self):
        """
        Display all file with paths

        Parameters
        ----------
        def printAllFile : TYPE
            DESCRIPTION.

        Returns
        -------
        None.

        """
        for file_loop in self.devfile:
            print(self.devfilepath[file_loop]+'/'+file_loop)

在您的测试文件中,您需要有这样的 main:

if __name__ == "__main__":
    import xmlrunner
    import sys
    
    if len(sys.argv) > 1:
        outputFolder = sys.argv.pop() #avoid conflic with unittest.main
    else:
        outputFolder = r'test-reports'
    print("Report will be created and store there: " + outputFolder)
    
    unittest.main(testRunner=xmlrunner.XMLTestRunner(output=outputFolder))

【讨论】:

【参考方案2】:

使用 Python 2.7 及更高版本,您不必编写新代码或使用第三方工具来执行此操作;通过命令行执行递归测试是内置的。将__init__.py 放在您的测试目录中,然后:

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

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


2021 年更新:

许多现代 Python 项目使用更高级的工具,例如 pytest。例如,下拉matplotlib 或scikit-learn,你会看到他们都在使用它。

了解这些较新的工具很重要,因为当您有超过 7000 个测试时,您需要:

更高级的方法来总结通过、跳过、警告、错误的内容 查看失败原因的简单方法 运行完成百分比 总运行时间 生成测试报告的方法 等等等等

【讨论】:

问题包括:ImportError: Start directory is not importable: 至少对于 Linux 上的 Python 2.7.8,命令行调用都没有给我递归。我的项目有几个子项目,它们的单元测试位于各自的“unit_tests//python/”目录中。如果我指定这样的路径,则运行该子项目的单元测试,但仅使用“unit_tests”作为测试目录参数没有找到测试(而不是所有子项目的所有测试,如我所愿)。有什么提示吗? 关于递归:第一个没有的命令默认为“.”并递归到子模块。也就是说,您想要发现的所有测试目录都需要有一个 init.py。如果他们这样做,他们将被发现命令找到。刚刚试了一下,效果不错。 这对我有用。我有一个包含四个文件的测试文件夹,从我的 Linux 终端运行它,很棒。 谢谢!为什么这不是公认的答案?在我看来,更好的答案总是不需要任何外部依赖...【参考方案3】:

我刚刚在我的基本测试目录中创建了一个 discover.py 文件,并为我的子目录中的任何内容添加了导入语句。然后 discover 能够通过在 discover.py 上运行它来找到我在这些目录中的所有测试

python -m unittest discover ./test -p '*.py'
# /test/discover.py
import unittest

from test.package1.mod1 import XYZTest
from test.package1.package2.mod2 import ABCTest
...

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

【讨论】:

【参考方案4】:

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

python -m unittest *_test.py

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

【讨论】:

【参考方案5】:

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

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

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

python -m unittest

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

【讨论】:

请注意,默认情况下它只搜索以“test”开头的文件名中的测试 没错,原来的问题是指“每个单元测试模块的形式是test_*.py。”,所以这个答案直接回复。我现在已将答案更新为更明确 谢谢,这是我使用 Travis Bear 的答案所缺少的。 我还需要将 init.py 文件添加到每个子文件夹中才能正常工作,否则很好。谢谢! 您能否更新您的答案以包括子目录也需要是包,以便您需要将 init.py 文件添加到测试目录内的子目录中?【参考方案6】:

此 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 文件来负担您的包/内存开销。

【讨论】:

【参考方案7】:

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

【讨论】:

我也试过这个方法,有几个测试,但效果很好。优秀!!!但我很好奇我只有 4 个测试。它们一起运行 0.032s,但是当我使用这种方法运行它们时,我得到结果 .... ---------------------------------------------------------------------- Ran 4 tests in 0.000s OK 为什么?区别,从何而来? 我无法从命令行运行一个看起来像这样的文件。应该如何调用? python file.py 完美运行!只需将其设置在您的 test/ 目录中,然后设置 start_id = "./" 。恕我直言,这个答案现在(Python 3.7)是公认的方式! 你可以把最后一行改成´res = runner.run(suite); sys.exit(0 if res.wasSuccessful() else 1)´ 如果你想要一个正确的退出代码【参考方案8】:

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

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 的子类。

【讨论】:

来自unittest.TestLoader docs:“通常不需要创建这个类的实例;unittest模块提供了一个可以共享为unittest.defaultTestLoader的实例。”此外,由于TestSuite 接受 iterable 作为参数,您可以在循环中构造所述可迭代以避免重复 loader.loadTestsFromTestCase @Two-Bit Alchemist 你的第二点特别好。我会更改代码以包含但我无法测试它。 (第一个 mod 会让我觉得它看起来太像 Java 了.. 虽然我意识到我是非理性的(把它们拧成驼峰式变量名)。 这是我的最爱,非常干净。能够将其打包并在我的常规命令行中作为参数。【参考方案9】:

通过稍微研究一下上面的代码(特别是使用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,查看递归测试发现的新答案 (***.com/a/24562019/104143) 您是否尝试过从测试实例对象运行测试?【参考方案10】:

如果是打包 库或应用程序,您不想这样做。 setuptoolswill do it for you。

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

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

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

然后运行python setup.py test

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

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

【讨论】:

不错的答案,它可用于在部署前自动化测试!谢谢【参考方案11】:

您可以使用可以为您执行此操作的测试运行程序。例如,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)

【讨论】:

这种方法的优势是仅仅将所有测试模块显式导入一个 test_all.py 模块并调用 unittest.main() ,您可以选择在某些模块中声明一个测试套件,而不是在别人身上? 我试过鼻子,效果很好。它很容易在我的项目中安装和运行。我什至可以用几行脚本来自动化它,在 virtualenv 中运行。 +1 鼻子! 并不总是可行的:有时导入项目的结构可能会导致鼻子在尝试在模块上运行导入时感到困惑。 请注意,nose“过去几年一直处于维护模式”,目前建议使用nose2、pytest,或者只是简单的unittest/unittest2用于新项目。 您是否尝试过从测试实例对象运行测试?【参考方案12】:

这是我创建a wrapper 以从命令行运行测试的方法:

#!/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

【讨论】:

这个答案的大部分与测试发现(即日志记录等)无关。 Stack Overflow 是用来回答问题的,而不是炫耀不相关的代码。【参考方案13】:

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

nose Py.Test

更多阅读:https://wiki.python.org/moin/PythonTestingToolsTaxonomy

【讨论】:

【参考方案14】:

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

这是我目前的解决方法:

import unittest

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

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

我将此代码放在我的测试目录中名为all 的模块中。如果我将此模块作为 LiClipse 的单元测试运行,那么所有测试都会运行。如果我要求只重复特定或失败的测试,那么只会运行那些测试。它也不会干扰我的命令行测试运行器(nosetests)——它被忽略了。

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

【讨论】:

所有测试文件和测试方法的名称应以“test_”开头。否则命令“Run as -> Python unit test”将找不到它们。【参考方案15】:

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

【讨论】:

【参考方案16】:

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

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

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

【讨论】:

【参考方案17】:

我使用了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。该问题与特定版本无关。 我在 Python 3.4 上,发现返回一个套件,使循环变得多余。 对于未来的 Larry:“Python 2.7 中的 unittest 添加了许多新功能,包括测试发现。unittest2 允许您在早期版本的 Python 中使用这些功能。”

以上是关于如何在目录中运行所有 Python 单元测试?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Python Django 中运行单元测试时禁用日志记录?

在 Dart 中运行所有单元测试

如何在 Python 中的每个单元测试之前和之后运行特定代码

python单元测试

如何使用 XML 输出运行 Python 单元测试

Jenkins 中的 Python 单元测试?