Как запустить модульное обнаружение из теста python setup.py?

Я пытаюсь понять, как заставить python setup.py test запускать эквивалент python -m unittest discover. Я не хочу использовать сценарий run_tests.py и не хочу использовать какие-либо внешние инструменты тестирования (например, nose или py.test). Ничего страшного, если решение работает только на python 2.7.

В setup.py, я думаю, мне нужно что-то добавить в поля test_suite и/или test_loader в конфиге, но я не могу найти комбинацию, которая работает правильно:

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

Возможно ли это, используя только unittest, встроенный в Python 2.7?

К вашему сведению, структура моего проекта выглядит так:

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. Укажите test_suite = 'unittest2.collector' в файле setup.py. Это запускает обнаружение тестов с параметрами по умолчанию из каталога, содержащего 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)

person cdwilson    schedule 08.06.2013    source источник
comment
Просто слово предостережения для тех, кто приходит сюда. Тест setup.py считается «запахом» кода и также считается устаревшим. github.com/pytest-dev/pytest-runner/issues/50   -  person Yashash Gaurav    schedule 09.10.2020


Ответы (7)


Если вы используете py27+ или py32+, решение довольно простое:

test_suite="tests",
person saschpe    schedule 12.02.2014
comment
Хотелось бы, чтобы это работало лучше, я столкнулся с этой проблемой: stackoverflow.com/questions/6164004/ Имена тестов должны совпадать с именами модулей. Если есть тест foo_test.py, то должен быть и соответствующий модуль foo.py. - person Charles L.; 31.12.2014
comment
Я согласен. В моем случае, когда я тестирую внешний Python, где буквально нет такого модуля Python с .py, похоже, нет хорошего способа добиться этого. - person Tom Swirly; 12.06.2016
comment
Это правильное решение. У меня не было проблемы @CharlesL. имел. Все мои тесты называются test_*.py. Кроме того, я обнаружил, что на самом деле он будет рекурсивно искать в заданном каталоге любой класс, расширяющий unittest.TestCast. Это чрезвычайно полезно, если у вас есть структура каталогов, в которой есть tests/first_batch/test_*.py и tests/second_batch/test_*.py. Вы можете просто указать test_suite="tests",, и он подберет все рекурсивно. Обратите внимание, что в каждом вложенном каталоге должен быть файл __init__.py. - person dcmm88; 14.07.2017

Из раздела Создание и распространение пакетов с помощью 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',
    ...
)
person Michael G    schedule 04.05.2016
comment
С этим решением есть проблема, потому что оно создает 2 уровня юнит-теста. Это означает, что setuptools создаст тестовую команду, которая попытается создать TestSuite из setup.my_test_suite, что заставит его импортировать setup.py, который снова запустит setup()! Во второй раз он создаст новую (вложенную) тестовую команду, которая запускает нужный тест. Это может быть незаметно для большинства людей, но если вы попытаетесь расширить тестовую команду (мне нужно было изменить ее, потому что я не могу запускать свои тесты «на месте»), вы можете столкнуться со странными проблемами. Вместо этого используйте stackoverflow.com/a/21726329/3272850. - person dcmm88; 14.07.2017
comment
Это приводит к тому, что тесты запускаются дважды для меня по причинам, упомянутым выше. Исправлено, переместив функцию в __init__.py папки с тестами и сославшись на нее. - person Anonymous; 13.10.2017
comment
Проблему с повторным выполнением тестов можно легко решить, выполнив функцию setup() внутри блока if __name__ == '__main__': в скрипте setup.py. При первом выполнении скрипта установки будет вызван блок if; во второй раз скрипт установки будет импортирован как модуль, поэтому блок if вызываться не будет. - person hoefling; 06.06.2018
comment
Хм, я понимаю, что мой setup.py вообще НЕ содержит этот параметр test_suite, но тест python setup.py по-прежнему отлично работает для меня. Это отличается от того, что документы говорят: если вы не установили test_suite в своем вызове setup() и не предоставили параметр --test-suite, произойдет ошибка. Любая идея? - person RayLuo; 08.07.2019

Вам не нужна конфигурация, чтобы заставить это работать. Есть в основном два основных способа сделать это:

Быстрый способ

Переименуйте свой 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.

Вот пример, сделанный кем-то в качестве ссылки.

person antimatter    schedule 26.04.2014
comment
Вопрос заключался в том, как заставить тест python setup.py использовать возможности обнаружения unittest. Это вообще не относится к этому. - person mikenerone; 03.05.2014
comment
Тьфу ... да, я полностью думал, что вопрос задавал что-то другое. Я не знаю, как это произошло, я, должно быть, схожу с ума :( - person antimatter; 03.05.2014

Одно из возможных решений — просто расширить команду test для distutils и setuptools/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)
person cdwilson    schedule 08.06.2013

Еще одно далеко не идеальное решение, слегка вдохновленное http://hg.python.org/unittest2/file/2b6411b9a838/unittest2/collector.py

Добавьте модуль, который возвращает TestSuite обнаруженных тестов. Затем настройте установку для вызова этого модуля.

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)
person cdwilson    schedule 08.06.2013

Модуль стандартной библиотеки 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,
    },
)
person mikenerone    schedule 03.05.2014
comment
Кстати, выше я предполагаю, что вы ориентируетесь только на версии Python, которые фактически поддерживают обнаружение (2.7 и 3.2+), поскольку вопрос касается именно этой функции. Конечно, вы можете обернуть вставку в проверку версии, если хотите сохранить совместимость и со старыми версиями (таким образом, используя в этих случаях стандартный загрузчик setuptools). - person mikenerone; 04.05.2014

Это не удалит 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 является единственным потребителем и вызывает ее в указанным способом.

person jwelsh    schedule 13.01.2015