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

我有一个包含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,但它是进步。但是我如何传递和显示结果的主要?或者说,我怎样才能让它工作,从而运行这个文件,并运行这个目录下的所有单元测试?

351167 次浏览

你可以用一个测试者来帮你做这件事。例如,鼻子非常好。运行时,它将在当前树中找到测试并运行它们。

更新:

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

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)

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

是的,用鼻子可能比用这个简单,但这不是重点。

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

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的重载来实现这个结果(我认为是最少的)代码行数:

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

使用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。例如,下拉matplotlibscikit-learn,你会看到它们都在使用它。

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

  • 更高级的方法来总结通过、跳过、警告和错误的内容
  • 很容易看出他们是如何失败的
  • 运行时的完成百分比
  • 总运行时间
  • 生成测试报告的方法
  • 等等

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

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

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

基于Stephen礼物的答案,我添加了对嵌套测试模块的支持。

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

我使用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运行这个模块作为单元测试,那么所有测试都将运行。如果我要求只重复特定或失败的测试,则只运行这些测试。它也不会干扰我的命令行测试运行器(nosetests)——它被忽略了。

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

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

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

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

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

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

然后你可以为所有测试的公共组件创建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

打包库或应用程序的情况下,你不想这样做。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中(参见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

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

在python 3中,如果你使用unittest.TestCase:

  • 你必须在你的test目录中有一个空(或其他)__init__.py文件(必须被命名为test/)
  • 你在test/中的测试文件匹配模式test_*.py
    • 它们可以在test/下的子目录中。这些subdirs可以被命名为任何东西,但它们都需要有一个__init__.py文件

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

python -m unittest

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

这个BASH脚本将从文件系统中的任意位置执行python unittest test目录,无论你在哪个工作目录中:它的工作目录总是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文件来增加包/内存开销。

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

python -m unittest *_test.py

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

我只是在我的基本测试目录中创建了一个discovery .py文件,并为子目录中的任何内容添加了导入语句。然后discover能够通过在discovery .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()

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

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

遇到了同样的问题。

解决方案是在每个文件夹中添加一个空的__init__.py,并使用python -m unittest discover -s

项目结构

tests/
__init__.py
domain/
value_object/
__init__.py
test_name.py
__init__.py
presentation/
__init__.py
test_app.py

然后运行命令

python -m unittest discover -s tests/domain

得到预期的结果

.
----------------------------------------------------------------------
Ran 1 test in 0.007s