如何从“ python setup.py test”运行单元测试发现?

我试图找出如何让 python setup.py test运行相当于 python -m unittest discover。我不想使用 run _ tests.py 脚本,也不想使用任何外部测试工具(如 nosepy.test)。如果该解决方案只适用于 python 2.7,那么没问题。

setup.py中,我想我需要在配置中为 test_suite和/或 test_loader字段添加一些内容,但是我似乎找不到一个能正确工作的组合:

config = {
'name': name,
'version': version,
'url': url,
'test_suite': '???',
'test_loader': '???',
}

只使用 Python 2.7内置的 unittest是否可行?

仅供参考,我的项目结构是这样的:

project/
package/
__init__.py
module.py
tests/
__init__.py
test_module.py
run_tests.py <- I want to delete this
setup.py

更新 : 使用 unittest2可以做到这一点,但是我希望只使用 unittest找到一些等价的东西

来自 https://pypi.python.org/pypi/unittest2

Unittest2包含一个非常基本的与 setuptools 兼容的测试收集器。在 setup.py 中指定 test _ kit = ‘ unittest2.Collection’。这将使用包含 setup.py 的目录中的默认参数启动测试发现,因此它可能是最有用的示例(参见 unittest2/Collector.py)。

现在,我只是使用一个名为 run_tests.py的脚本,但是我希望我可以通过转向一个只使用 python setup.py test的解决方案来摆脱这个问题。

下面是我希望移除的 run_tests.py:

import unittest


if __name__ == '__main__':


# use the default shared TestLoader instance
test_loader = unittest.defaultTestLoader


# use the basic test runner that outputs to sys.stderr
test_runner = unittest.TextTestRunner()


# automatically discover all tests in the current dir of the form test*.py
# NOTE: only works for python 2.7 and later
test_suite = test_loader.discover('.')


# run the test suite
test_runner.run(test_suite)
72808 次浏览

One possible solution is to simply extend the test command for distutilsand setuptools/distribute. This seems like a total kluge and way more complicated than I would prefer, but seems to correctly discover and run all the tests in my package upon running python setup.py test. I'm holding off on selecting this as the answer to my question in hopes that someone will provide a more elegant solution :)

(Inspired by https://docs.pytest.org/en/latest/goodpractices.html#integrating-with-setuptools-python-setup-py-test-pytest-runner)

Example setup.py:

try:
from setuptools import setup
except ImportError:
from distutils.core import setup


def discover_and_run_tests():
import os
import sys
import unittest


# get setup.py directory
setup_file = sys.modules['__main__'].__file__
setup_dir = os.path.abspath(os.path.dirname(setup_file))


# use the default shared TestLoader instance
test_loader = unittest.defaultTestLoader


# use the basic test runner that outputs to sys.stderr
test_runner = unittest.TextTestRunner()


# automatically discover all tests
# NOTE: only works for python 2.7 and later
test_suite = test_loader.discover(setup_dir)


# run the test suite
test_runner.run(test_suite)


try:
from setuptools.command.test import test


class DiscoverTest(test):


def finalize_options(self):
test.finalize_options(self)
self.test_args = []
self.test_suite = True


def run_tests(self):
discover_and_run_tests()


except ImportError:
from distutils.core import Command


class DiscoverTest(Command):
user_options = []


def initialize_options(self):
pass


def finalize_options(self):
pass


def run(self):
discover_and_run_tests()


config = {
'name': 'name',
'version': 'version',
'url': 'http://example.com',
'cmdclass': {'test': DiscoverTest},
}


setup(**config)

Another less than ideal solution slightly inspired by http://hg.python.org/unittest2/file/2b6411b9a838/unittest2/collector.py

Add a module that returns a TestSuite of discovered tests. Then configure setup to call that module.

project/
package/
__init__.py
module.py
tests/
__init__.py
test_module.py
discover_tests.py
setup.py

Here's discover_tests.py:

import os
import sys
import unittest


def additional_tests():
setup_file = sys.modules['__main__'].__file__
setup_dir = os.path.abspath(os.path.dirname(setup_file))
return unittest.defaultTestLoader.discover(setup_dir)

And here's setup.py:

try:
from setuptools import setup
except ImportError:
from distutils.core import setup


config = {
'name': 'name',
'version': 'version',
'url': 'http://example.com',
'test_suite': 'discover_tests',
}


setup(**config)

If you use py27+ or py32+, the solution is pretty simple:

test_suite="tests",

You don't need config to get this working. There are basically two main ways to do it:

The quick way

Rename your test_module.py to module_test.py (basically add _test as a suffix to tests for a particular module), and python will find it automatically. Just make sure to add this to setup.py:

from setuptools import setup, find_packages


setup(
...
test_suite = 'tests',
...
)

The long way

Here's how to do it with your current directory structure:

project/
package/
__init__.py
module.py
tests/
__init__.py
test_module.py
run_tests.py <- I want to delete this
setup.py

Under tests/__init__.py, you want to import the unittest and your unit test script test_module, and then create a function to run the tests. In tests/__init__.py, type in something like this:

import unittest
import test_module


def my_module_suite():
loader = unittest.TestLoader()
suite = loader.loadTestsFromModule(test_module)
return suite

The TestLoader class has other functions besides loadTestsFromModule. You can run dir(unittest.TestLoader) to see the other ones, but this one is the simplest to use.

Since your directory structure is such, you'll probably want the test_module to be able to import your module script. You might have already done this, but just in case you didn't, you could include the parent path so that you can import the package module and the module script. At the top of your test_module.py, type:

import os, sys
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))


import unittest
import package.module
...

Then finally, in setup.py, include the tests module and run the command you created, my_module_suite:

from setuptools import setup, find_packages


setup(
...
test_suite = 'tests.my_module_suite',
...
)

Then you just run python setup.py test.

Here is a sample someone made as a reference.

Python's standard library unittest module supports discovery (in Python 2.7 and later, and Python 3.2 and later). If you can assume those minimum versions, then you can just add the discover command line argument to the unittest command.

Only a small tweak is needed to setup.py:

import setuptools.command.test
from setuptools import (find_packages, setup)


class TestCommand(setuptools.command.test.test):
""" Setuptools test command explicitly using test discovery. """


def _test_args(self):
yield 'discover'
for arg in super(TestCommand, self)._test_args():
yield arg


setup(
...
cmdclass={
'test': TestCommand,
},
)

This won't remove run_tests.py, but will make it work with setuptools. Add:

class Loader(unittest.TestLoader):
def loadTestsFromNames(self, names, _=None):
return self.discover(names[0])

Then in setup.py: (I assume you're doing something like setup(**config))

config = {
...
'test_loader': 'run_tests:Loader',
'test_suite': '.', # your start_dir for discover()
}

The only downside I see is it's bending the semantics of loadTestsFromNames, but the setuptools test command is the only consumer, and calls it in a specified way.

From Building and Distributing Packages with Setuptools (emphasis mine):

test_suite

A string naming a unittest.TestCase subclass (or a package or module containing one or more of them, or a method of such a subclass), or naming a function that can be called with no arguments and returns a unittest.TestSuite.

Hence, in setup.py you would add a function that returns a TestSuite:

import unittest
def my_test_suite():
test_loader = unittest.TestLoader()
test_suite = test_loader.discover('tests', pattern='test_*.py')
return test_suite

Then, you would specify the command setup as follows:

setup(
...
test_suite='setup.my_test_suite',
...
)