如何从“python setup.py test”运行 unittest discover?

Posted

技术标签:

【中文标题】如何从“python setup.py test”运行 unittest discover?【英文标题】:How to run unittest discover from "python setup.py test"? 【发布时间】:2013-06-04 18:12:39 【问题描述】:

我试图弄清楚如何让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_suite = 'unittest2.collector'。这将使用包含 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)

【问题讨论】:

对碰巧来到这里的人请注意。 setup.py test 被认为是代码“气味”,也被设置为弃用。 github.com/pytest-dev/pytest-runner/issues/50 【参考方案1】:

一种可能的解决方案是简单地将test 命令扩展为distutilssetuptools/distribute。这似乎比我希望的要复杂得多,但似乎在运行python setup.py test 时正确地发现并运行了我的包中的所有测试。我推迟选择这个作为我问题的答案,希望有人能提供更优雅的解决方案:)

(灵感来自https://docs.pytest.org/en/latest/goodpractices.html#integrating-with-setuptools-python-setup-py-test-pytest-runner)

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

【讨论】:

【参考方案2】:

受http://hg.python.org/unittest2/file/2b6411b9a838/unittest2/collector.py 启发的另一个不太理想的解决方案

添加一个返回 TestSuite 的已发现测试的模块。然后配置 setup 以调用该模块。

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

这里是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)

这里是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)

【讨论】:

【参考方案3】:

如果你使用py27+或者py32+,解决方法很简单:

test_suite="tests",

【讨论】:

我希望这能更好地工作,我遇到了这个问题:***.com/questions/6164004/… "测试名称应该匹配模块名称。如果有一个 "foo_test.py" 测试,需要有一个相应的模块 foo .py。” 我同意。在我的情况下,我正在测试一个 Python 外部,实际上没有带有 .py 的 Python 模块,似乎没有很好的方法来实现这一点。 这是正确的解决方案。我没有问题@CharlesL。有。我所有的测试都命名为test_*.py。此外,我发现它实际上会递归搜索给定目录以查找扩展unittest.TestCastany 类。如果您的目录结构中有tests/first_batch/test_*.pytests/second_batch/test_*.py,这将非常有用。您可以指定test_suite="tests",,它会递归地获取所有内容。请注意,每个嵌套目录都需要有一个 __init__.py 文件。 您需要使用from setuptools import setup 而不是from distutils.core import setup 才能使此选项存在。我不知道为什么官方文档似乎使用 distutils.core,我猜这是默认的? ***.com/a/58534041 对此和其他论点有很好的描述。【参考方案4】:

您不需要配置即可使其正常工作。基本上有两种主要的方法:

快捷方式

将您的test_module.py 重命名为module_test.py(基本上添加_test 作为特定模块的测试后缀),python 会自动找到它。只需确保将其添加到setup.py

from setuptools import setup, find_packages

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

漫漫长路

以下是使用当前目录结构的方法:

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

tests/__init__.py 下,您要导入unittest 和您的单元测试脚本test_module,然后创建一个函数来运行测试。在tests/__init__.py 中,输入如下内容:

import unittest
import test_module

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

TestLoader 类除了loadTestsFromModule 之外还有其他功能。你可以运行dir(unittest.TestLoader)来查看其他的,但是这个是最简单的。

由于您的目录结构是这样的,您可能希望test_module 能够导入您的module 脚本。您可能已经这样做了,但以防万一,您可以包含父路径,以便您可以导入 package 模块和 module 脚本。在test_module.py 的顶部,输入:

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

import unittest
import package.module
...

最后,在setup.py 中包含tests 模块并运行您创建的命令my_module_suite

from setuptools import setup, find_packages

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

然后你只需运行python setup.py test

这是一个sample 某人作为参考。

【讨论】:

问题是如何让“python setup.py test”使用unittest的发现能力。这根本没有解决这个问题。 呃......是的,我完全认为这个问题是在问一些不同的东西。我不知道这是怎么回事,我一定是疯了:(【参考方案5】:

Python 的标准库 unittest 模块支持发现(在 Python 2.7 及更高版本以及 Python 3.2 及更高版本中)。如果您可以假设这些最低版本,那么您只需将discover 命令行参数添加到unittest 命令。

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

【讨论】:

顺便说一句,我在上面假设您只针对实际支持发现的 Python 版本(2.7 和 3.2+),因为问题是关于这个特性的。当然,如果您想保持与旧版本的兼容性,您也可以将插入包含在版本检查中(因此在这些情况下使用 setuptools 的标准加载器)。【参考方案6】:

这不会删除 run_tests.py,但会使其与 setuptools 一起使用。添加:

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

然后在 setup.py 中:(我假设你正在做类似setup(**config) 的事情)

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

我看到的唯一缺点是它扭曲了loadTestsFromNames 的语义,但 setuptools test 命令是唯一的消费者,并在specified way 中调用它。

【讨论】:

【参考方案7】:

来自Building and Distributing Packages with Setuptools(强调我的):

测试套件

命名 unittest.TestCase 子类(或包或模块)的字符串 包含其中一个或多个,或此类子类的方法),或命名 可以不带参数调用并返回 unittest.TestSuite 的函数

因此,在 setup.py 中,您将添加一个返回 TestSuite 的函数:

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

然后,您将指定命令setup,如下所示:

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

【讨论】:

这个解决方案有问题,因为它创建了2个“级别”的单元测试。这意味着 setuptools 将创建一个“测试”命令,该命令将尝试从 setup.my_test_suite 创建一个 TestSuite,这将强制它导入 setup.py,这将再次运行 setup()!这第二次它将创建一个新的(嵌套的)测试命令来运行您想要的测试。这对大多数人来说可能并不明显,但是如果您尝试扩展测试命令(我需要修改它,因为我无法“就地”运行我的测试),您可能会遇到奇怪的问题。请改用***.com/a/21726329/3272850 由于上述原因,这会导致测试为我运行两次。通过将函数移动到测试文件夹的__init__.py 并引用它来修复它。 测试执行两次的问题可以通过在if __name__ == '__main__': 脚本中的if __name__ == '__main__': 块中执行setup() 函数来轻松解决。第一次执行设置脚本时,将调用 if 块;第二次设置脚本将作为模块导入,因此不会调用 if 块。 嗯,我意识到我的 setup.py 根本不包含 test_suite 参数,但“python setup.py test”对我来说仍然可以正常工作。这与the docs says 不同:“如果您没有在 setup() 调用中设置 test_suite,并且不提供 --test-suite 选项,则会发生错误。”有什么想法吗?

以上是关于如何从“python setup.py test”运行 unittest discover?的主要内容,如果未能解决你的问题,请参考以下文章

如何从外部从 GitHub 加载 JavaScript 文件? [复制]

如何将数据从回收器适配器发送到片段 |如何从 recyclerview 适配器调用片段函数

如何从 Firebase 获取所有设备令牌?

如何直接从类调用从接口继承的方法?

如何从服务器获取和设置 android 中的 API(从服务器获取 int 值)?如何绑定和实现这个

如何从Mac从android studio中的fabric注销? [复制]