关于python:使用典型的测试目录结构运行unittest

Running unittest with typical test directory structure

对于一个简单的python模块来说,最常见的目录结构似乎是将单元测试分离到它们自己的test目录中:

1
2
3
4
5
6
7
new_project/
    antigravity/
        antigravity.py
    test/
        test_antigravity.py
    setup.py
    etc.

例如,请参阅这个python项目howto。

我的问题是,实际运行测试的通常方式是什么?我怀疑这对除了我以外的所有人都是显而易见的,但是您不能从测试目录运行python test_antigravity.py,因为它的import antigravity会失败,因为模块不在路径上。

我知道我可以修改pythonpath和其他与搜索路径相关的技巧,但我不能相信这是最简单的方法——如果你是开发人员,这很好,但是如果你的用户只是想检查测试是否通过,那么期望他们使用它是不现实的。

另一种选择是将测试文件复制到另一个目录中,但是它看起来有点愚蠢,并且忽略了将它们放在单独目录中开始的要点。

所以,如果您刚刚将源代码下载到我的新项目中,您将如何运行单元测试?我更喜欢一个能让我对我的用户说:"运行单元测试需要做X。"


我认为最好的解决方案是使用unittest命令行接口,它将目录添加到sys.path中,这样您就不必(在TestLoader类中完成)。

例如,对于这样的目录结构:

1
2
3
new_project
├── antigravity.py
└── test_antigravity.py

您只需运行:

1
2
$ cd new_project
$ python -m unittest test_antigravity

对于像您这样的目录结构:

1
2
3
4
5
6
7
new_project
├── antigravity
│   ├── __init__.py         # make it a package
│   └── antigravity.py
└── test
    ├── __init__.py         # also make test a package
    └── test_antigravity.py

test包内的测试模块中,您可以像往常一样导入antigravity包及其模块:

1
2
3
4
5
6
7
8
# 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

1
2
$ cd new_project
$ python -m unittest test.test_antigravity

只需以导入的方式引用测试模块。

运行单个测试用例或测试方法:

也可以运行单个TestCase或单个测试方法:

1
2
$ python -m unittest test.test_antigravity.GravityTestCase
$ python -m unittest test.test_antigravity.GravityTestCase.test_method

运行所有测试:

您还可以使用测试发现来发现和运行所有测试,它们必须是名为test*.py的模块或包(可以用-p, --pattern标志更改):

1
2
$ cd new_project
$ python -m unittest discover

这将运行test包内的所有test*.py模块。


对于您的用户来说,最简单的解决方案是提供一个可执行脚本(runtests.py或某些类似脚本),它可以引导必要的测试环境,包括在需要时将根项目目录临时添加到sys.path。这不需要用户设置环境变量,在引导脚本中这样可以很好地工作:

1
2
3
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定位在正确的位置就可以了。

另外,python 2.7+中的unittest模块(在python 2.6及更早版本中是unittest2的后端口)现在内置了测试发现,因此,如果需要自动测试发现,nose就不再是必需的了:您的用户指令可以像"python-m unittest discover"一样简单。


我通常在项目目录中创建一个"运行测试"脚本(源目录和test共同使用的脚本),它加载我的"所有测试"套件。这通常是样板代码,所以我可以在项目之间重用它。

运行:

1
2
3
4
import unittest
import test.all_tests
testSuite = test.all_tests.create_test_suite()
text_runner = unittest.TextTestRunner().run(testSuite)

test/all_tests.py(如何在目录中运行所有python单元测试?)

1
2
3
4
5
6
7
8
9
10
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。缺点是您需要更多的支持代码来执行特定的测试…我只是每次都运行它们。


从您链接到的文章中:

Create a test_modulename.py file and
put your unittest tests in it. Since
the test modules are in a separate
directory from your code, you may need
to add your module’s parent directory
to your PYTHONPATH in order to run
them:

1
2
3
4
5
$ cd /path/to/googlemaps

$ export PYTHONPATH=$PYTHONPATH:/path/to/googlemaps/googlemaps

$ python test/test_googlemaps.py

Finally, there is one more popular
unit testing framework for Python
(it’s that important!), nose. nose
helps simplify and extend the builtin
unittest framework (it can, for
example, automagically find your test
code and setup your PYTHONPATH for
you), but it is not included with the
standard Python distribution.

也许你应该照它的意思看鼻子?


如果运行"python setup.py develop",那么包将位于路径中。但您可能不想这样做,因为您可能会影响系统的python安装,这就是为什么存在类似virtualenv和buildout的工具的原因。


我也有同样的问题,有一个单独的单元测试文件夹。根据上述建议,我将绝对源路径添加到sys.path

以下解决方案的好处是,可以运行文件test/test_yourmodule.py,而不首先更改到测试目录中:

1
2
3
4
5
6
7
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模块的解决方案/示例

考虑到以下项目结构:

1
2
3
4
5
6
7
8
9
ProjectName
 ├── project_name
 |    ├── models
 |    |    └── thing_1.py
 |    └── __main__.py
 └── test
      ├── models
      |    └── test_thing_1.py
      └── __main__.py

您可以使用python project_name从根目录运行项目,该目录调用ProjectName/project_name/__main__.py

要使用python test运行测试,有效地运行ProjectName/test/__main__.py,需要执行以下操作:

1)通过添加一个__init__.py文件,将您的test/models目录转换为一个包。这使得子目录中的测试用例可以从父目录test访问。

1
2
3
# ProjectName/test/models/__init__.py

from .test_thing_1 import Thing1TestCase

2)在test/__main__.py中修改您的系统路径,以包含project_name目录。

1
2
3
4
5
6
7
8
9
10
11
# 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导入内容。

1
2
3
4
5
6
7
8
9
10
11
# 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)

使用setup.py develop使您的工作目录成为已安装的python环境的一部分,然后运行测试。


如果您使用VS代码,并且您的测试与您的项目位于同一级别,那么运行和调试代码是不可能的。您可以更改launch.json文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
   "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

1
"envFile":"${workspaceRoot}/.env",

在项目的根目录中,添加.env文件

在.env文件中,将路径添加到项目的根目录。这将临时添加

PYTHONPATH=C:\YOUR\PYTHON\PROJECT
OOT_DIRECTORY

到项目的路径,您将能够使用VS代码中的调试单元测试


我注意到,如果您从"src"目录运行unittest命令行接口,那么导入就可以在不做修改的情况下正常工作。

1
python -m unittest discover -s ../test

如果要将其放入项目目录中的批处理文件中,可以执行以下操作:

1
setlocal & cd src & python -m unittest discover -s ../test

可以使用运行选定测试或所有测试的包装器。

例如:

1
./run_tests antigravity/*.py

或者使用globbing(tests/**/*.py)递归运行所有测试(由shopt -s globstar启用)。

包装器基本上可以使用argparse来解析以下参数:

1
2
parser = argparse.ArgumentParser()
parser.add_argument('files', nargs='*')

然后加载所有测试:

1
2
for filename in args.files:
    exec(open(filename).read())

然后将它们添加到您的测试套件中(使用inspect):

1
2
3
4
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))

然后运行它们:

1
result = unittest.TextTestRunner(verbosity=2).run(alltests)

有关详细信息,请查看此示例。

另请参见:如何在一个目录中运行所有Python单元测试?


以下是我的项目结构:

1
2
3
4
5
6
ProjectFolder:
 - project:
     - __init__.py
     - item.py
 - tests:
     - test_item.py

我发现最好在setup()方法中导入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()

What's the usual way of actually running the tests

我使用python 3.6.2

1
2
3
cd new_project

pytest test/test_antigravity.py

安装pytest:sudo pip install pytest

我没有设置任何路径变量,我的导入也没有在相同的"测试"项目结构下失败。

我评论了一下:if __name__ == '__main__'是这样的:

测试反重力.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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()

Python 3 +

添加到@pierre

使用这样的unittest目录结构:

1
2
3
4
5
6
7
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

1
2
$ cd new_project
$ python -m unittest test.test_antigravity

或单个TestCase

1
$ python -m unittest test.test_antigravity.GravityTestCase

强制不要忘记__init__.py,即使是空的,否则不会起作用。


如果没有巫毒,就不能从父目录导入。下面是另一种至少与Python3.6一起工作的方法。

首先,使用包含以下内容的test/context.py文件:

1
2
3
import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

然后在文件test/test_antigvity.py中导入以下内容:

1
2
3
4
5
6
import unittest
try:
    import context
except ModuleNotFoundError:
    import test.context    
import antigravity

请注意,此try except子句的原因是

  • 当使用"python test_antigvity.py"运行时,import test.context失败,并且
  • 当从新的_项目目录中使用"python-m unittest"运行时,导入上下文失败。

有了这个诡计,他们两个都起作用了。

现在,您可以使用以下命令运行测试目录中的所有测试文件:

1
2
3
$ pwd
/projects/new_project
$ python -m unittest

或运行单独的测试文件:

1
2
$ cd test
$ python test_antigravity

好吧,这并不比在test-antigvity.py中包含context.py的内容更漂亮,但可能有一点。欢迎提出建议。


这个bash脚本将从文件系统的任何地方执行python unittest目录,不管您在哪个工作目录中。

当您停留在./src./example工作目录中,并且需要快速单元测试时,这非常有用:

1
2
3
4
5
6
7
#!/bin/bash

this_program="$0"
dirname="`dirname $this_program`"
readlink="`readlink -e $dirname`"

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

不需要使用test/__init__.py文件来负担生产期间的包/内存开销。


如果要查找仅限命令行的解决方案:

基于以下目录结构(使用专用源目录进行通用化):

1
2
3
4
5
new_project/
    src/
        antigravity.py
    test/
        test_antigravity.py

窗口:(在new_project中)

1
2
$ set PYTHONPATH=%PYTHONPATH%;%cd%\src
$ python -m unittest discover -s test

如果您想在批处理for循环中使用这个问题,请参阅这个问题。

linux:(在new_project中)

1
2
$ 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添加更多目录。


如果测试目录中有多个目录,那么必须向每个目录添加一个__init__.py文件。

1
2
3
4
5
6
7
8
9
/home/johndoe/snakeoil
└── test
    ├── __init__.py        
    └── frontend
        └── __init__.py
        └── test_foo.py
    └── backend
        └── __init__.py
        └── test_bar.py

然后要一次运行每个测试,请运行:

1
python -m unittest discover -s /home/johndoe/snakeoil/test -t /home/johndoe/snakeoil

资料来源:python -m unittest -h

1
2
3
4
5
  -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)

你真的应该使用pip工具。

使用pip install -e .以开发模式安装软件包。这是Pytest推荐的一个非常好的实践(请参阅它们的良好实践文档,您还可以在其中找到两个要遵循的项目布局)。