即使是一个简单的 Python 模块,非常常见的目录结构似乎是将单元测试分离到它们自己的 test
目录中:
new_project/
antigravity/
antigravity.py
test/
test_antigravity.py
setup.py
etc.
我的问题很简单实际运行测试的常用方法是什么?我怀疑这对除了我之外的每个人来说都是显而易见的,但你不能只从测试目录运行 python test_antigravity.py
作为它的 import antigravity
将失败,因为模块不在路径上。
我知道我可以修改 PYTHONPATH 和其他与搜索路径相关的技巧,但我不敢相信这是最简单的方法 - 如果您是开发人员,这很好,但如果他们只想检查测试,期望您的用户使用是不现实的通过。
另一种选择是将测试文件复制到另一个目录中,但这似乎有点愚蠢,并且错过了将它们放在单独的目录中开始的意义。
那么,如果您刚刚将源代码下载到我的新项目中,您将如何运行单元测试?我更喜欢一个能让我对我的用户说的答案:“运行单元测试做 X。”
unittest
命令行界面,因此您不必将目录添加到路径中。
我认为最好的解决方案是使用 unittest
command line interface,它将目录添加到 sys.path
,这样您就不必(在 TestLoader
类中完成)。
例如对于这样的目录结构:
new_project
├── antigravity.py
└── test_antigravity.py
你可以运行:
$ cd new_project
$ python -m unittest test_antigravity
对于像你这样的目录结构:
new_project
├── antigravity
│ ├── __init__.py # make it a package
│ └── antigravity.py
└── test
├── __init__.py # also make test a package
└── test_antigravity.py
在 test
包内的测试模块中,您可以照常导入 antigravity
包及其模块:
# import the package
import antigravity
# import the antigravity module
from antigravity import antigravity
# or an object inside the antigravity module
from antigravity.antigravity import my_object
运行单个测试模块:
要运行单个测试模块,在本例中为 test_antigravity.py
:
$ cd new_project
$ python -m unittest test.test_antigravity
只需像导入它一样引用测试模块。
运行单个测试用例或测试方法:
您还可以运行单个 TestCase
或单个测试方法:
$ python -m unittest test.test_antigravity.GravityTestCase
$ python -m unittest test.test_antigravity.GravityTestCase.test_method
运行所有测试:
您还可以使用 test discovery,它将为您发现并运行所有测试,它们必须是名为 test*.py
的模块或包(可以使用 -p, --pattern
标志更改):
$ cd new_project
$ python -m unittest discover
$ # Also works without discover for Python 3
$ # as suggested by @Burrito in the comments
$ python -m unittest
这将运行 test
包内的所有 test*.py
模块。
对您的用户而言,最简单的解决方案是提供一个可执行脚本(runtests.py
或类似的脚本)来引导必要的测试环境,包括(如果需要)将您的根项目目录临时添加到 sys.path
。这不需要用户设置环境变量,像这样在引导脚本中可以正常工作:
import sys, os
sys.path.insert(0, os.path.dirname(__file__))
然后,您对用户的说明可以像“python runtests.py
”一样简单。
当然,如果你真正需要的路径是os.path.dirname(__file__)
,那么你根本不需要将它添加到sys.path
中; Python 总是将当前运行脚本的目录放在 sys.path
的开头,因此根据您的目录结构,只需将 runtests.py
放在正确的位置即可。
此外,unittest module in Python 2.7+(在 Python 2.6 及更早版本中向后移植为 unittest2)现在内置了 test discovery,因此如果您想要自动化测试发现,则不再需要鼻子:您的用户说明可以像python -m unittest discover
。
python -m pdb tests\test_antigravity.py
的情况下,这个 hack 很有用。在 pdb 中,我执行了 sys.path.insert(0, "antigravity")
,它允许 import 语句解析,就好像我正在运行模块一样。
我有同样的问题很长时间了。我最近选择的是以下目录结构:
project_path
├── Makefile
├── src
│ ├── script_1.py
│ ├── script_2.py
│ └── script_3.py
└── tests
├── __init__.py
├── test_script_1.py
├── test_script_2.py
└── test_script_3.py
在 test 文件夹的 __init__.py
脚本中,我编写了以下内容:
import os
import sys
PROJECT_PATH = os.getcwd()
SOURCE_PATH = os.path.join(
PROJECT_PATH,"src"
)
sys.path.append(SOURCE_PATH)
共享项目非常重要的是 Makefile,因为它强制正确运行脚本。这是我在 Makefile 中输入的命令:
run_tests:
python -m unittest discover .
Makefile 很重要,不仅因为它运行的命令,还因为 从哪里运行它。如果您在测试中 cd 并执行 python -m unittest discover .
,它不会起作用,因为 unit_tests 中的 init 脚本调用 os.getcwd(),然后它会指向不正确的绝对路径(即附加到 sys.path 并且您将丢失您的源文件夹)。脚本会在发现找到所有测试后运行,但它们不会正常运行。所以 Makefile 是为了避免记住这个问题。
我真的很喜欢这种方法,因为我不必碰我的 src 文件夹、我的单元测试或我的环境变量,并且一切都运行顺利。
我通常在加载我的“所有测试”套件的项目目录(源目录和 test
通用的那个)中创建一个“运行测试”脚本。这通常是样板代码,所以我可以在项目之间重用它。
run_tests.py:
import unittest
import test.all_tests
testSuite = test.all_tests.create_test_suite()
text_runner = unittest.TextTestRunner().run(testSuite)
test/all_tests.py(来自 How do I run all Python unit tests in a directory?)
import glob
import unittest
def create_test_suite():
test_file_strings = glob.glob('test/test_*.py')
module_strings = ['test.'+str[5:len(str)-3] for str in test_file_strings]
suites = [unittest.defaultTestLoader.loadTestsFromName(name) \
for name in module_strings]
testSuite = unittest.TestSuite(suites)
return testSuite
通过此设置,您确实可以在您的测试模块中include antigravity
。缺点是你需要更多的支持代码来执行特定的测试......我每次都运行它们。
run tests
脚本,并找到 a lot cleaner way 来执行此操作。强烈推荐。
从您链接到的文章中:
创建一个 test_modulename.py 文件并将您的单元测试测试放入其中。由于测试模块位于与您的代码不同的目录中,您可能需要将模块的父目录添加到 PYTHONPATH 才能运行它们: $ cd /path/to/googlemaps $ export PYTHONPATH=$PYTHONPATH:/path/to /googlemaps/googlemaps $ python test/test_googlemaps.py 最后,还有一个更流行的 Python 单元测试框架(这很重要!),nose。鼻子有助于简化和扩展内置的 unittest 框架(例如,它可以自动找到您的测试代码并为您设置 PYTHONPATH),但它不包含在标准 Python 发行版中。
也许您应该按照它的建议查看 nose?
我有同样的问题,有一个单独的单元测试文件夹。根据上述建议,我将 绝对源路径 添加到 sys.path
。
以下解决方案的好处是,可以运行文件 test/test_yourmodule.py
而无需首先更改到测试目录:
import sys, os
testdir = os.path.dirname(__file__)
srcdir = '../antigravity'
sys.path.insert(0, os.path.abspath(os.path.join(testdir, srcdir)))
import antigravity
import unittest
Python unittest 模块的解决方案/示例
给定以下项目结构:
ProjectName
├── project_name
| ├── models
| | └── thing_1.py
| └── __main__.py
└── test
├── models
| └── test_thing_1.py
└── __main__.py
您可以使用调用 ProjectName/project_name/__main__.py
的 python project_name
从根目录运行您的项目。
要使用 python test
运行测试,有效地运行 ProjectName/test/__main__.py
,您需要执行以下操作:
1) 通过添加一个 __init__.py
文件将您的 test/models
目录变成一个包。这使得子目录中的测试用例可以从父 test
目录访问。
# ProjectName/test/models/__init__.py
from .test_thing_1 import Thing1TestCase
2) 修改 test/__main__.py
中的系统路径以包含 project_name
目录。
# ProjectName/test/__main__.py
import sys
import unittest
sys.path.append('../project_name')
loader = unittest.TestLoader()
testSuite = loader.discover('test')
testRunner = unittest.TextTestRunner(verbosity=2)
testRunner.run(testSuite)
现在,您可以在测试中成功地从 project_name
导入内容。
# ProjectName/test/models/test_thing_1.py
import unittest
from project_name.models import Thing1 # this doesn't work without 'sys.path.append' per step 2 above
class Thing1TestCase(unittest.TestCase):
def test_thing_1_init(self):
thing_id = 'ABC'
thing1 = Thing1(thing_id)
self.assertEqual(thing_id, thing.id)
如果你运行“python setup.py develop”,那么包将在路径中。但是您可能不想这样做,因为您可能会感染您的系统 python 安装,这就是存在 virtualenv 和 buildout 之类的工具的原因。
我注意到,如果您从“src”目录运行 unittest 命令行界面,则导入无需修改即可正常工作。
python -m unittest discover -s ../test
如果要将其放在项目目录中的批处理文件中,可以执行以下操作:
setlocal & cd src & python -m unittest discover -s ../test
如果您使用 VS Code 并且您的测试与您的项目位于同一级别,那么运行和调试您的代码将无法开箱即用。您可以做的是更改您的 launch.json 文件:
{
"version": "0.2.0",
"configurations": [
{
"name": "Python",
"type": "python",
"request": "launch",
"stopOnEntry": false,
"pythonPath": "${config:python.pythonPath}",
"program": "${file}",
"cwd": "${workspaceRoot}",
"env": {},
"envFile": "${workspaceRoot}/.env",
"debugOptions": [
"WaitOnAbnormalExit",
"WaitOnNormalExit",
"RedirectOutput"
]
}
]
}
这里的关键是 envFile
"envFile": "${workspaceRoot}/.env",
在项目的根目录中添加 .env 文件
在 .env 文件中添加项目根目录的路径。这将暂时添加
PYTHONPATH=C:\YOUR\PYTHON\PROJECT\ROOT_DIRECTORY
到您的项目的路径,您将能够使用 VS Code 中的调试单元测试
使用 setup.py develop
使您的工作目录成为已安装 Python 环境的一部分,然后运行测试。
invalid command 'develop'
,如果我要求 setup.py --help-commands
,则不会提及此选项。 setup.py
本身是否需要某些东西才能使其工作?
setup.py
文件中缺少一个 import setuptools
。但我想这确实表明这不会一直适用于其他人的模块。
pip install -e .
与 python setup.py develop
完全相同,它只是对您的 setup.py
进行猴子补丁以使用 setuptools,即使它实际上并没有,所以无论哪种方式都可以。
可以使用运行选定或所有测试的包装器。
例如:
./run_tests antigravity/*.py
或以递归方式运行所有测试,使用 globbing (tests/**/*.py
)(由 shopt -s globstar
启用)。
包装器基本上可以使用 argparse
来解析参数,例如:
parser = argparse.ArgumentParser()
parser.add_argument('files', nargs='*')
然后加载所有测试:
for filename in args.files:
exec(open(filename).read())
然后将它们添加到您的测试套件中(使用 inspect
):
alltests = unittest.TestSuite()
for name, obj in inspect.getmembers(sys.modules[__name__]):
if inspect.isclass(obj) and name.startswith("FooTest"):
alltests.addTest(unittest.makeSuite(obj))
并运行它们:
result = unittest.TextTestRunner(verbosity=2).run(alltests)
查看 this 示例以获取更多详细信息。
另请参阅:How to run all Python unit tests in a directory?
蟒蛇 3+
添加到@Pierre
像这样使用 unittest
目录结构:
new_project
├── antigravity
│ ├── __init__.py # make it a package
│ └── antigravity.py
└── test
├── __init__.py # also make test a package
└── test_antigravity.py
要运行测试模块 test_antigravity.py
:
$ cd new_project
$ python -m unittest test.test_antigravity
或单个 TestCase
$ python -m unittest test.test_antigravity.GravityTestCase
强制不要忘记 __init__.py
,即使为空,否则将不起作用。
没有一些巫术,你不能从父目录导入。这是另一种至少适用于 Python 3.6 的方法。
首先,有一个包含以下内容的文件 test/context.py:
import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
然后在文件 test/test_antigravity.py 中导入以下内容:
import unittest
try:
import context
except ModuleNotFoundError:
import test.context
import antigravity
请注意,这个 try-except 子句的原因是
使用“python test_antigravity.py”运行时导入 test.context 失败,并且
使用 new_project 目录中的“python -m unittest”运行时,导入上下文失败。
有了这个诡计,他们都可以工作。
现在您可以使用以下命令运行测试目录中的所有测试文件:
$ pwd
/projects/new_project
$ python -m unittest
或运行单个测试文件:
$ cd test
$ python test_antigravity
好吧,这并不比在 test_antigravity.py 中包含 context.py 的内容更漂亮,但也许有一点。欢迎提出建议。
以下是我的项目结构:
ProjectFolder:
- project:
- __init__.py
- item.py
- tests:
- test_item.py
我发现在 setUp() 方法中导入更好:
import unittest
import sys
class ItemTest(unittest.TestCase):
def setUp(self):
sys.path.insert(0, "../project")
from project import item
# further setup using this import
def test_item_props(self):
# do my assertions
if __name__ == "__main__":
unittest.main()
实际运行测试的通常方式是什么
我使用 Python 3.6.2
cd new_project
pytest test/test_antigravity.py
要安装 pytest:sudo pip install pytest
我没有设置任何路径变量,并且我的导入没有因相同的“测试”项目结构而失败。
我注释掉了这些东西:if __name__ == '__main__'
像这样:
test_antigravity.py
import antigravity
class TestAntigravity(unittest.TestCase):
def test_something(self):
# ... test stuff here
# if __name__ == '__main__':
#
# if __package__ is None:
#
# import something
# sys.path.append(path.dirname(path.dirname(path.abspath(__file__))))
# from .. import antigravity
#
# else:
#
# from .. import antigravity
#
# unittest.main()
你真的应该使用 pip 工具。
使用 pip install -e .
在开发模式下安装您的包。这是一个非常好的做法,由 pytest 推荐(请参阅他们的 good practices documentation,您还可以在其中找到两个要遵循的项目布局)。
pytest
运行测试要好得多,因为你得到的控制台输出是彩色的,带有堆栈跟踪信息和详细的断言错误信息。
如果您的测试目录中有多个目录,则必须向每个目录添加一个 __init__.py
文件。
/home/johndoe/snakeoil
└── test
├── __init__.py
└── frontend
└── __init__.py
└── test_foo.py
└── backend
└── __init__.py
└── test_bar.py
然后一次运行每个测试,运行:
python -m unittest discover -s /home/johndoe/snakeoil/test -t /home/johndoe/snakeoil
来源:python -m unittest -h
-s START, --start-directory START
Directory to start discovery ('.' default)
-t TOP, --top-level-directory TOP
Top level directory of project (defaults to start
directory)
此 BASH 脚本将从文件系统中的任何位置执行 python unittest 测试目录,无论您在哪个工作目录中。
这在停留在 ./src
或 ./example
工作目录并且您需要快速单元测试时很有用:
#!/bin/bash
this_program="$0"
dirname="`dirname $this_program`"
readlink="`readlink -e $dirname`"
python -m unittest discover -s "$readlink"/test -v
在生产过程中,无需 test/__init__.py
文件来负担您的包/内存开销。
通过这种方式,您可以从任何地方运行测试脚本,而不会在命令行中弄乱系统变量。
这会将主项目文件夹添加到 python 路径,找到的位置相对于脚本本身,而不是相对于当前工作目录。
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
将其添加到所有测试脚本的顶部。这会将主项目文件夹添加到系统路径,因此从那里工作的任何模块导入现在都可以工作。从哪里运行测试并不重要。
您显然可以更改 project_path_hack 文件以匹配您的主项目文件夹位置。
基于 *nix 的系统(macOS、Linux)的简单解决方案;并且可能还有 Windows 上的 Git bash。
PYTHONPATH=$PWD python test/test_antigravity.py
与 pytest test/test_antigravity.py
不同,print
语句很容易工作。 “脚本”的完美方式,但不是真正的单元测试。
当然,我想做一个适当的自动化测试,我会考虑用适当的设置pytest
。
如果您正在寻找仅命令行的解决方案:
基于以下目录结构(用专门的源目录概括):
new_project/
src/
antigravity.py
test/
test_antigravity.py
Windows:(在 new_project
中)
$ set PYTHONPATH=%PYTHONPATH%;%cd%\src
$ python -m unittest discover -s test
如果您想在批处理 for 循环中使用它,请参阅 this question。
Linux:(在 new_project
中)
$ export PYTHONPATH=$PYTHONPATH:$(pwd)/src [I think - please edit this answer if you are a Linux user and you know this]
$ python -m unittest discover -s test
使用这种方法,如果需要,还可以向 PYTHONPATH 添加更多目录。
您项目中的 unittest 有 setup.py
文件。尝试:
python3 setup.py build
和
python3 setup.py develop --user
做配置路径的工作等等。试试看!
test*.py
,python -m unittest discover
将在test
目录中查找并运行测试。如果您将子目录命名为tests
,请使用python -m unittest discover -s tests
,如果您将测试文件命名为antigravity_test.py
,请使用python -m unittest discover -s tests -p '*test.py'
文件名可以使用下划线,但不能使用破折号。ImportError: No module named 'test.test_antigravity'
。也许专家可以确认并将答案子目录名称更改为例如“测试”(复数)。import antigravity
和from antigravity import antigravity
,我的test_antigravity.py
仍然会引发导入错误。我有两个__init_.py
文件,我正在从new project
目录调用python3 -m unittest discover
。还有什么可能是错的?test/__init__.py
在这里很重要,即使是空的test
是特殊的……但只是为了记录,它不是。 :Ppython -m unittest discover
和test/
一样适用于tests/
中的测试文件。